csrf-protection
SKILL.md
CSRF Protection
Prevent Cross-Site Request Forgery attacks on your web application.
When to Use
- Implementing forms that change state
- Building APIs consumed by browsers
- Setting up session cookies
- Reviewing authentication flows
- Any state-changing POST/PUT/DELETE requests
How CSRF Works
<!-- Attacker's malicious page -->
<html>
<body onload="document.forms[0].submit()">
<form action="https://bank.com/transfer" method="POST">
<input name="to" value="attacker" />
<input name="amount" value="10000" />
</form>
</body>
</html>
<!-- Victim visits this page while logged into bank.com -->
<!-- Their session cookie is sent automatically! -->
Protection Methods
1. SameSite Cookies (Primary Defense)
// Express session with SameSite
app.use(session({
secret: process.env.SESSION_SECRET,
cookie: {
httpOnly: true,
secure: true, // HTTPS only
sameSite: 'strict', // Or 'lax' for better UX
maxAge: 3600000
}
}));
// Set-Cookie header result:
// Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Strict
SameSite Options:
| Value | Behavior |
|---|---|
Strict |
Cookie never sent cross-site |
Lax |
Sent on top-level navigation (default) |
None |
Always sent (requires Secure) |
2. CSRF Tokens (Defense in Depth)
const csrf = require('csurf');
// Setup CSRF middleware
const csrfProtection = csrf({
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict'
}
});
// Apply to routes
app.get('/form', csrfProtection, (req, res) => {
res.render('form', { csrfToken: req.csrfToken() });
});
app.post('/submit', csrfProtection, (req, res) => {
// Token automatically validated by middleware
// Process form...
});
<!-- In your form template -->
<form method="POST" action="/submit">
<input type="hidden" name="_csrf" value="<%= csrfToken %>">
<!-- Other form fields -->
<button type="submit">Submit</button>
</form>
3. Double Submit Cookie Pattern
// Generate CSRF token
function generateCsrfToken() {
return crypto.randomBytes(32).toString('hex');
}
// Set token in cookie and return for form
app.get('/form', (req, res) => {
const token = generateCsrfToken();
res.cookie('csrf-token', token, {
httpOnly: false, // JS needs to read this
secure: true,
sameSite: 'strict'
});
res.render('form', { csrfToken: token });
});
// Validate both match
app.post('/submit', (req, res) => {
const cookieToken = req.cookies['csrf-token'];
const bodyToken = req.body._csrf || req.headers['x-csrf-token'];
if (!cookieToken || !bodyToken ||
!crypto.timingSafeEqual(Buffer.from(cookieToken), Buffer.from(bodyToken))) {
return res.status(403).json({ error: 'Invalid CSRF token' });
}
// Process request...
});
4. Custom Header Verification (for APIs)
// Require custom header that can't be set cross-origin
function csrfHeaderCheck(req, res, next) {
// Browsers block cross-origin custom headers
const csrfHeader = req.headers['x-requested-with'];
if (csrfHeader !== 'XMLHttpRequest') {
return res.status(403).json({ error: 'CSRF validation failed' });
}
next();
}
// Apply to API routes
app.use('/api', csrfHeaderCheck);
// Client-side
fetch('/api/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest' // Custom header
},
body: JSON.stringify(data),
credentials: 'include'
});
5. Origin/Referer Validation
function validateOrigin(req, res, next) {
const origin = req.headers.origin || req.headers.referer;
if (!origin) {
// Might be same-origin request - additional checks needed
return next();
}
try {
const url = new URL(origin);
const allowedOrigins = [
'https://example.com',
'https://www.example.com'
];
if (!allowedOrigins.includes(url.origin)) {
return res.status(403).json({ error: 'Invalid origin' });
}
} catch {
return res.status(403).json({ error: 'Invalid origin header' });
}
next();
}
Framework-Specific Implementation
React (with fetch)
// Get CSRF token from meta tag or cookie
function getCsrfToken() {
return document.querySelector('meta[name="csrf-token"]')?.content
|| document.cookie.match(/csrf-token=([^;]+)/)?.[1];
}
// Custom fetch wrapper
async function secureFetch(url, options = {}) {
const csrfToken = getCsrfToken();
return fetch(url, {
...options,
credentials: 'include',
headers: {
...options.headers,
'X-CSRF-Token': csrfToken
}
});
}
// Usage
await secureFetch('/api/update', {
method: 'POST',
body: JSON.stringify(data)
});
Django
# settings.py - CSRF is enabled by default
MIDDLEWARE = [
'django.middleware.csrf.CsrfViewMiddleware',
# ...
]
# In templates
<form method="post">
{% csrf_token %}
<!-- form fields -->
</form>
# For AJAX
<script>
const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value;
fetch('/api/endpoint', {
method: 'POST',
headers: {
'X-CSRFToken': csrftoken
},
body: JSON.stringify(data)
});
</script>
Rails
# ApplicationController
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
end
# In views
<%= form_with url: '/submit' do |f| %>
<!-- CSRF token automatically included -->
<% end %>
# For AJAX (Rails UJS handles this automatically)
# Or manually:
headers: {
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
}
Laravel
// In Blade templates
<form method="POST" action="/submit">
@csrf
<!-- form fields -->
</form>
// For AJAX - token in meta tag
<meta name="csrf-token" content="{{ csrf_token() }}">
// JavaScript
fetch('/api/endpoint', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
}
});
Common Mistakes
// MISTAKE 1: CSRF token in URL (visible in logs/history)
<a href="/delete?csrf=abc123">Delete</a> // BAD
// MISTAKE 2: Not validating on state-changing requests
app.get('/delete/:id', deleteHandler); // GET shouldn't change state
// MISTAKE 3: Accepting token from any location
const token = req.query.csrf || req.body.csrf; // Don't check query!
// MISTAKE 4: SameSite=None without understanding
cookie: { sameSite: 'none', secure: true } // Opens CSRF risk
// MISTAKE 5: Not regenerating token after login
// Token should change when auth state changes
Testing CSRF Protection
<!-- Test page (host on different domain) -->
<!DOCTYPE html>
<html>
<body>
<h1>CSRF Test</h1>
<!-- Test 1: Form submission -->
<form id="test1" action="http://target.com/api/update" method="POST">
<input name="data" value="malicious">
</form>
<!-- Test 2: Fetch request -->
<script>
fetch('http://target.com/api/update', {
method: 'POST',
credentials: 'include',
body: JSON.stringify({ data: 'malicious' })
}).then(r => console.log('Fetch result:', r.status));
</script>
<!-- Test 3: Image tag (for GET requests) -->
<img src="http://target.com/api/delete?id=1" />
</body>
</html>
Code Review Checklist
- SameSite cookie attribute set (Strict or Lax)
- CSRF tokens on all state-changing forms
- Tokens validated server-side
- Tokens regenerated on authentication changes
- No state changes on GET requests
- Origin/Referer validated for sensitive operations
- CORS properly configured
- Custom headers required for API calls
Best Practices
- Use SameSite Cookies: First line of defense
- Add CSRF Tokens: Defense in depth
- Validate Origin: Extra layer for sensitive ops
- No State Changes on GET: REST properly
- Regenerate Tokens: After login/privilege change
- Use Framework Defaults: Don't disable built-in protection
Weekly Installs
2
Repository
latestaiagents/…t-skillsGitHub Stars
2
First Seen
Feb 4, 2026
Installed on
mcpjam2
claude-code2
replit2
junie2
windsurf2
zencoder2