Cross-Site Request Forgery (CSRF) is a web security vulnerability that allows attackers to trick users into performing unwanted actions on websites where they’re authenticated. Despite being well-documented for over two decades, CSRF continues to plague modern web applications due to common implementation mistakes and misconceptions.
One of the most frequent sources of confusion? Developers often mistake CSRF errors for CORS errors, leading to hours of debugging in the wrong direction. Understanding the difference between these two security mechanisms—and knowing how to properly test and isolate them—is crucial for building secure web applications.
In this comprehensive guide, we’ll explore the most common CSRF protection mistakes developers make, show you how to distinguish CSRF from CORS issues using practical tools, and provide actionable solutions to secure your applications.
Table of Contents
- Understanding CSRF Attacks
- CORS vs CSRF: Understanding the Confusion
- How to Distinguish CSRF from CORS Errors
- Common CSRF Protection Mistakes
- 1. Confusing CORS with CSRF Protection
- 2. Missing CSRF Tokens Entirely
- 3. Using GET Requests for State-Changing Operations
- 4. Improper SameSite Cookie Configuration
- 5. Weak CSRF Token Implementation
- 6. Not Protecting API Endpoints
- 7. CSRF Tokens in URL Parameters
- 8. Ignoring Subdomain Security
- 9. Inadequate SPA Protection
- 10. File Upload CSRF Vulnerabilities
- Best Practices for CSRF Protection
- Testing Your CSRF Defenses
- Conclusion
Understanding CSRF Attacks
Before diving into common mistakes, let’s briefly understand how CSRF attacks work:
CSRF Attack Flow:
- A user authenticates to a legitimate website (e.g.,
bank.com) and receives a session cookie - The user visits a malicious website while still authenticated
- The malicious site triggers a request to
bank.com(e.g., transfer funds) - The browser automatically includes the session cookie with the request
- The legitimate website processes the request as if it came from the user
The key issue: browsers automatically include cookies with requests, regardless of where the request originated.
CORS vs CSRF: Understanding the Confusion
Many developers confuse CORS (Cross-Origin Resource Sharing) and CSRF (Cross-Site Request Forgery) because both involve cross-origin requests and security. However, they protect against completely different threats:
CORS (Cross-Origin Resource Sharing)
- Purpose: Controls which origins can read responses from your API
- Enforced by: Browser
- Protects: Your API’s data from being read by unauthorized origins
- Does NOT prevent: Requests from being sent or executed
CSRF (Cross-Site Request Forgery)
- Purpose: Prevents unauthorized requests from being executed with user credentials
- Enforced by: Server-side validation
- Protects: Against attackers tricking users into performing unwanted actions
- Does NOT control: Who can read API responses
The Critical Misunderstanding
Here’s what developers often get wrong:
// This configuration DOES NOT protect against CSRF!
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', 'https://trusted-site.com');
res.setHeader('Access-Control-Allow-Credentials', 'true');
next();
});
Why CORS doesn’t prevent CSRF:
- Browsers send the request with cookies attached before checking CORS
- State-changing operations execute on the server
- CORS only blocks JavaScript from reading the response
- Forms and simple requests bypass CORS entirely
How to Distinguish CSRF from CORS Errors
When you encounter errors during development, determining whether they’re CORS-related or CSRF-related can save hours of debugging. Here’s a systematic approach to isolate the issue.
Step 1: Identify the Error Message
CORS errors look like this:
Access to fetch at 'https://api.example.com/data' from origin 'https://myapp.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present.
CSRF errors look like this:
403 Forbidden: CSRF token missing or invalid
419 Page Expired (Laravel)
403 Forbidden (generic)
Step 2: Use a CORS Proxy to Isolate the Issue
One of the fastest ways to determine if your problem is CORS-related is to temporarily route your requests through a CORS proxy like corsproxy.io. Here’s how:
Original request (might be failing):
fetch('https://api.example.com/data', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ data: 'value' })
});
Test with corsproxy.io:
// Route through CORS proxy to bypass CORS
fetch('https://corsproxy.io/?url=https://api.example.com/data', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ data: 'value' })
});
Interpreting the results:
- If it works through the proxy: Your issue is CORS-related. The proxy added the necessary CORS headers.
- If it still fails with 403/419: Your issue is likely CSRF-related. The proxy doesn’t change authentication or CSRF validation.
- If you get a different error: You’ve isolated the problem and can debug the actual issue.
Step 3: Check Browser DevTools Network Tab
Open DevTools and examine:
- Request Headers: Is your CSRF token being sent?
- Response Headers: Are CORS headers present?
- Status Code: 403/419 suggests CSRF, CORS errors show in console
- Cookies: Are session cookies being sent with
credentials: 'include'?
Real-World Debugging Example
// You're getting errors calling your API
async function debugAPICall() {
console.log('Test 1: Direct call');
try {
const response = await fetch('https://api.example.com/transfer', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount: 100 })
});
console.log('Direct call succeeded:', response.status);
} catch (error) {
console.error('Direct call failed:', error.message);
}
console.log('Test 2: Through corsproxy.io');
try {
const response = await fetch('https://corsproxy.io/?url=https://api.example.com/transfer', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount: 100 })
});
console.log('Proxy call succeeded:', response.status);
// If proxy works but direct doesn't = CORS issue
// If both fail with 403 = CSRF issue
// If proxy fails but direct works = Check proxy configuration
} catch (error) {
console.error('Proxy call failed:', error.message);
}
}
🚀 Try corsproxy.io: Use our CORS proxy during development to quickly identify whether your errors are CORS or CSRF related. Get started for free with 5,000 requests per month.
Common CSRF Protection Mistakes
Now that you can distinguish CORS from CSRF issues, let’s explore the most common CSRF protection mistakes developers make.
1. Confusing CORS with CSRF Protection
The Mistake:
The most dangerous misconception in web security: believing that CORS headers protect against CSRF attacks.
Why It Doesn’t Work:
// This DOES NOT protect against CSRF!
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', 'https://trusted-site.com');
next();
});
CORS restricts which origins can read responses, but it doesn’t prevent requests from being sent. Browsers will still:
- Send the request with cookies attached
- Execute state-changing operations on the server
- Only block the JavaScript code from reading the response
The Attack Still Works:
<!-- Malicious site can still trigger CSRF -->
<form action="https://bank.com/transfer" method="POST">
<input type="hidden" name="amount" value="10000">
<input type="hidden" name="to" value="attacker-account">
</form>
<script>
document.forms[0].submit();
</script>
This form submission:
- Includes session cookies automatically
- Isn’t blocked by CORS (forms don’t trigger CORS preflight)
- Executes on the server before CORS checks apply
How to Test This:
Use corsproxy.io to verify that CORS and CSRF are separate concerns:
// Even with CORS properly configured through a proxy,
// CSRF protection is still required
fetch('https://corsproxy.io/?url=https://your-api.com/delete-account', {
method: 'POST',
credentials: 'include'
// Missing CSRF token - should fail even though CORS is bypassed
});
The Solution:
Implement dedicated CSRF protection regardless of your CORS configuration:
// Express with csurf middleware
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });
app.post('/transfer', csrfProtection, (req, res) => {
// Verify CSRF token before processing
const { amount, to } = req.body;
// Process transfer
});
2. Missing CSRF Tokens Entirely
The Mistake:
Developers sometimes rely solely on authentication mechanisms (like JWT or session cookies) without implementing CSRF tokens, assuming authentication is sufficient.
Why It’s Vulnerable:
// Vulnerable API endpoint
app.post('/api/delete-account', authenticateUser, (req, res) => {
// If only checking authentication, attacker can trigger this
deleteAccount(req.user.id);
res.json({ success: true });
});
Attack Example:
<!-- Attacker's page -->
<img src="https://api.example.com/api/delete-account" style="display:none">
Even though the user is authenticated, the attacker can trigger the request because browsers automatically send cookies.
The Solution:
Implement CSRF tokens for all state-changing operations:
// Server generates token
app.get('/api/csrf-token', (req, res) => {
const token = generateSecureToken();
req.session.csrfToken = token;
res.json({ csrfToken: token });
});
// Server validates token
app.post('/api/delete-account', authenticateUser, (req, res) => {
const token = req.headers['x-csrf-token'];
if (!token || token !== req.session.csrfToken) {
return res.status(403).json({ error: 'Invalid CSRF token' });
}
deleteAccount(req.user.id);
res.json({ success: true });
});
// Client includes token
fetch('/api/delete-account', {
method: 'POST',
headers: {
'X-CSRF-Token': csrfToken,
'Content-Type': 'application/json'
},
credentials: 'include'
});
3. Using GET Requests for State-Changing Operations
The Mistake:
Using GET requests for operations that modify server state makes CSRF attacks trivially easy.
Vulnerable Code:
// NEVER DO THIS!
app.get('/delete-user/:id', (req, res) => {
deleteUser(req.params.id);
res.redirect('/users');
});
Attack:
<!-- Attacker can embed this anywhere -->
<img src="https://example.com/delete-user/123">
Why It’s Especially Dangerous:
- No CSRF protection needed for the attack
- Can be embedded in images, CSS, emails
- May be cached by browsers/proxies
- Violates HTTP specification (GET should be safe and idempotent)
The Solution:
Always use POST, PUT, DELETE, or PATCH for state-changing operations:
// ✓ Correct implementation
app.post('/delete-user/:id', csrfProtection, (req, res) => {
deleteUser(req.params.id);
res.json({ success: true });
});
Exception for Confirmations:
If you must use GET (like email confirmation links), use single-use, cryptographically secure tokens:
// Email confirmation - acceptable GET usage
app.get('/confirm-email/:token', async (req, res) => {
const token = req.params.token;
// Token should be:
// 1. Cryptographically random
// 2. Single-use
// 3. Time-limited
// 4. Tied to specific user
const isValid = await verifyEmailToken(token);
if (isValid) {
await confirmUserEmail(token);
res.render('email-confirmed');
} else {
res.status(400).render('invalid-token');
}
});
4. Improper SameSite Cookie Configuration
The Mistake:
Not setting the SameSite attribute on cookies, or setting it incorrectly, leaving applications vulnerable to CSRF attacks.
Missing SameSite:
// Vulnerable - no SameSite attribute
res.cookie('session', sessionId, {
httpOnly: true,
secure: true
// Missing: sameSite
});
Incorrect SameSite:
// Too permissive - allows cross-site requests
res.cookie('session', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'none' // DANGEROUS without proper CSRF protection!
});
The Solution:
Use SameSite=Lax or SameSite=Strict based on your needs:
// ✓ Recommended for most applications
res.cookie('session', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'lax' // Blocks CSRF for POST requests
});
// ✓ For highly sensitive operations
res.cookie('session', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'strict' // Blocks all cross-site cookie sending
});
SameSite Values Explained:
Strict: Cookie never sent on cross-site requests (even on navigation)Lax: Cookie sent on top-level navigation with GET, but not on cross-site POST/PUT/DELETENone: Cookie sent with all requests (requiresSecureflag, only for specific use cases)
Important Caveat:
SameSite cookies are a defense-in-depth measure but shouldn’t be your only CSRF protection:
- Older browsers don’t support SameSite
- Safari has quirks with SameSite handling
- Some legitimate use cases require SameSite=None
5. Weak CSRF Token Implementation
The Mistake:
Implementing CSRF tokens in ways that undermine their security.
Common Weak Implementations:
// Predictable tokens
const csrfToken = Date.now().toString();
// Reusing tokens across sessions
const csrfToken = 'static-token-for-all-users';
// Not tying token to user session
app.post('/transfer', (req, res) => {
if (req.body.csrf === 'valid-token') { // Any user can use any token
processTransfer();
}
});
// Client-side token generation
// JavaScript generates token - attacker can replicate
const csrfToken = Math.random().toString(36);
The Solution:
Implement cryptographically secure, session-specific tokens:
const crypto = require('crypto');
// Generate secure token
function generateCSRFToken() {
return crypto.randomBytes(32).toString('hex');
}
// Tie token to user session
app.use((req, res, next) => {
if (!req.session.csrfToken) {
req.session.csrfToken = generateCSRFToken();
}
res.locals.csrfToken = req.session.csrfToken;
next();
});
// Validate token
function validateCSRFToken(req) {
const token = req.body._csrf || req.headers['x-csrf-token'];
return token && token === req.session.csrfToken;
}
app.post('/api/action', (req, res) => {
if (!validateCSRFToken(req)) {
return res.status(403).json({ error: 'Invalid CSRF token' });
}
// Process request
});
Double Submit Cookie Pattern:
An alternative approach that doesn’t require server-side session storage:
// Set CSRF token as cookie and require it in header
app.use((req, res, next) => {
const token = req.cookies.csrfToken || generateCSRFToken();
res.cookie('csrfToken', token, {
httpOnly: false, // JavaScript needs to read this
secure: true,
sameSite: 'strict'
});
next();
});
// Validate: cookie and header must match
function validateDoubleSubmit(req) {
const cookieToken = req.cookies.csrfToken;
const headerToken = req.headers['x-csrf-token'];
return cookieToken && headerToken && cookieToken === headerToken;
}
6. Not Protecting API Endpoints
The Mistake:
Assuming API endpoints are safe from CSRF because they’re “not for browsers” or use JSON.
Vulnerable API:
// Developers think this is safe because it's an API
app.post('/api/v1/users/:id/promote', authenticateUser, (req, res) => {
promoteUserToAdmin(req.params.id);
res.json({ success: true });
});
Attack Works:
// Attacker's page
fetch('https://api.example.com/api/v1/users/attacker-id/promote', {
method: 'POST',
credentials: 'include', // Includes cookies
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({})
});
Why APIs Are Still Vulnerable:
- If using cookie-based authentication, CSRF applies
- Even with CORS, the request is sent (only response is blocked)
- State-changing operations still execute
Testing API CSRF Protection:
You can use corsproxy.io to test if your API is vulnerable:
// Test if CSRF protection is working
// Even when CORS is bypassed, CSRF should still block this
fetch('https://corsproxy.io/?url=https://your-api.com/admin/promote', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: 'attacker' })
});
// Should get 403 Forbidden if CSRF protection is working
// Getting 200 OK means you're vulnerable!
The Solution:
Protect API endpoints that use cookie-based authentication:
// Option 1: CSRF tokens for cookie-based auth
app.post('/api/v1/users/:id/promote',
authenticateUser,
csrfProtection,
(req, res) => {
promoteUserToAdmin(req.params.id);
res.json({ success: true });
}
);
// Option 2: Use Bearer token authentication (not cookies)
app.post('/api/v1/users/:id/promote', (req, res) => {
const token = req.headers.authorization?.replace('Bearer ', '');
const user = validateJWTToken(token); // From header, not cookie
if (!user) {
return res.status(401).json({ error: 'Unauthorized' });
}
promoteUserToAdmin(req.params.id);
res.json({ success: true });
});
Key Principle:
- Cookie-based auth → CSRF protection required
- Token-based auth (header only) → CSRF protection not required
7. CSRF Tokens in URL Parameters
The Mistake:
Including CSRF tokens in URL query parameters instead of headers or request bodies.
Vulnerable Implementation:
// Don't do this!
app.post('/delete-account?csrf=abc123', (req, res) => {
if (req.query.csrf === req.session.csrfToken) {
deleteAccount(req.user.id);
}
});
Why It’s Dangerous:
- Referer Leakage: CSRF tokens in URLs can leak via Referer headers
- Browser History: Tokens stored in browser history
- Server Logs: Tokens logged in access logs
- Shared Links: Users might share URLs with tokens
- Proxy Caching: URLs with parameters might be cached
The Solution:
Always include CSRF tokens in headers or request bodies:
// ✓ In custom header (recommended for APIs)
fetch('/api/action', {
method: 'POST',
headers: {
'X-CSRF-Token': csrfToken,
'Content-Type': 'application/json'
},
body: JSON.stringify({ data: 'value' })
});
// ✓ In request body (recommended for forms)
<form method="POST" action="/action">
<input type="hidden" name="_csrf" value="<%= csrfToken %>">
<button type="submit">Submit</button>
</form>
8. Ignoring Subdomain Security
The Mistake:
Not considering that CSRF tokens and cookies can be vulnerable across subdomains.
Vulnerable Cookie Configuration:
// Cookie accessible from all subdomains
res.cookie('session', sessionId, {
domain: '.example.com', // Accessible by *.example.com
httpOnly: true,
secure: true
});
Attack Scenario:
- Attacker compromises or controls a subdomain (e.g.,
malicious.example.com) - Attacker sets cookies for the parent domain
- Attacker can read/modify cookies from the main domain
- CSRF protection bypassed
The Solution:
// ✓ Don't set domain attribute (restricts to exact host)
res.cookie('session', sessionId, {
// domain not set - only valid for current subdomain
httpOnly: true,
secure: true,
sameSite: 'strict'
});
// If you must share across subdomains, scope CSRF tokens
app.use((req, res, next) => {
const subdomain = req.hostname;
req.session.csrfTokens = req.session.csrfTokens || {};
if (!req.session.csrfTokens[subdomain]) {
req.session.csrfTokens[subdomain] = generateCSRFToken();
}
res.locals.csrfToken = req.session.csrfTokens[subdomain];
next();
});
Additional Subdomain Protection:
- Isolate sensitive operations to specific subdomains
- Use separate session cookies for different security levels
- Implement additional authentication for cross-subdomain requests
9. Inadequate SPA Protection
The Mistake:
Single Page Applications (SPAs) not properly managing CSRF tokens, especially during token refresh or page reloads.
Common SPA Issues:
// Token fetched once but never refreshed
function App() {
const [csrfToken, setCsrfToken] = useState(null);
useEffect(() => {
fetch('/api/csrf-token')
.then(r => r.json())
.then(data => setCsrfToken(data.token));
}, []); // Only runs once!
// Token might expire or become invalid
}
// Token not included in interceptor
axios.post('/api/action', { data: 'value' });
// Missing CSRF token header!
The Solution:
Implement proper token management in your SPA:
// React example with axios interceptor
import axios from 'axios';
import { useState, useEffect } from 'react';
// Configure axios to always include CSRF token
axios.interceptors.request.use(
(config) => {
const token = document.cookie
.split('; ')
.find(row => row.startsWith('csrfToken='))
?.split('=')[1];
if (token) {
config.headers['X-CSRF-Token'] = token;
}
return config;
},
(error) => Promise.reject(error)
);
// Handle token refresh on 403 responses
axios.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 403) {
// Token might be invalid, fetch new one
await axios.get('/api/csrf-token');
// Retry original request
return axios.request(error.config);
}
return Promise.reject(error);
}
);
// React component
function useCSRFToken() {
const [token, setToken] = useState(null);
useEffect(() => {
// Fetch token on mount
axios.get('/api/csrf-token')
.then(response => setToken(response.data.token));
// Refresh token periodically
const interval = setInterval(() => {
axios.get('/api/csrf-token')
.then(response => setToken(response.data.token));
}, 30 * 60 * 1000); // Every 30 minutes
return () => clearInterval(interval);
}, []);
return token;
}
Vue.js Example:
// Vue 3 with axios
import axios from 'axios';
const api = axios.create({
baseURL: '/api',
withCredentials: true
});
// Add CSRF token to all requests
api.interceptors.request.use((config) => {
const token = document.querySelector('meta[name="csrf-token"]')?.content;
if (token) {
config.headers['X-CSRF-Token'] = token;
}
return config;
});
export default api;
10. File Upload CSRF Vulnerabilities
The Mistake:
Forgetting to protect file upload endpoints with CSRF tokens, assuming multipart form data is safe.
Vulnerable Upload Endpoint:
// Missing CSRF protection
app.post('/upload-avatar', upload.single('avatar'), (req, res) => {
const user = req.user;
user.avatar = req.file.path;
user.save();
res.json({ success: true });
});
Attack:
<!-- Attacker's page -->
<form action="https://example.com/upload-avatar" method="POST" enctype="multipart/form-data">
<input type="file" name="avatar" value="malicious.jpg">
</form>
<script>
// Pre-fill file and auto-submit
const file = new File(['malicious content'], 'malicious.jpg');
const dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
document.querySelector('input[type="file"]').files = dataTransfer.files;
document.forms[0].submit();
</script>
The Solution:
Always protect file uploads with CSRF tokens:
// ✓ Protected upload endpoint
app.post('/upload-avatar',
csrfProtection,
upload.single('avatar'),
(req, res) => {
const user = req.user;
user.avatar = req.file.path;
user.save();
res.json({ success: true });
}
);
<!-- HTML form with CSRF token -->
<form action="/upload-avatar" method="POST" enctype="multipart/form-data">
<input type="hidden" name="_csrf" value="<%= csrfToken %>">
<input type="file" name="avatar" accept="image/*">
<button type="submit">Upload Avatar</button>
</form>
For AJAX Upload:
// Fetch API with FormData and CSRF token
const formData = new FormData();
formData.append('avatar', fileInput.files[0]);
fetch('/upload-avatar', {
method: 'POST',
headers: {
'X-CSRF-Token': csrfToken
},
body: formData,
credentials: 'include'
});
Best Practices for CSRF Protection
1. Defense in Depth
Don’t rely on a single protection mechanism. Combine multiple approaches:
// Multiple layers of protection
app.post('/critical-action',
rateLimiter, // Rate limiting
authenticateUser, // Authentication
csrfProtection, // CSRF token validation
checkUserPermissions, // Authorization
(req, res) => {
// Process request
}
);
// Set security headers
app.use((req, res, next) => {
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('Content-Security-Policy', "default-src 'self'");
next();
});
2. Use Established Libraries
Don’t roll your own CSRF protection:
// Express
const csrf = require('csurf');
app.use(csrf({ cookie: true }));
// Django (Python)
# Automatically enabled with middleware
MIDDLEWARE = [
'django.middleware.csrf.CsrfViewMiddleware',
]
// Laravel (PHP)
// Automatically included via VerifyCsrfToken middleware
// Rails (Ruby)
# Automatically enabled
protect_from_forgery with: :exception
3. Educate Your Team
Common team education points:
- CORS ≠ CSRF protection
- Always use POST/PUT/DELETE for state changes
- Cookie-based auth requires CSRF protection
- Test CSRF defenses in code reviews
4. Regular Security Audits
// Automated testing for CSRF vulnerabilities
describe('CSRF Protection', () => {
it('should reject requests without CSRF token', async () => {
const response = await request(app)
.post('/api/delete-account')
.set('Cookie', sessionCookie);
expect(response.status).toBe(403);
});
it('should reject requests with invalid CSRF token', async () => {
const response = await request(app)
.post('/api/delete-account')
.set('Cookie', sessionCookie)
.set('X-CSRF-Token', 'invalid-token');
expect(response.status).toBe(403);
});
it('should accept requests with valid CSRF token', async () => {
const response = await request(app)
.post('/api/delete-account')
.set('Cookie', sessionCookie)
.set('X-CSRF-Token', validToken);
expect(response.status).toBe(200);
});
});
Testing Your CSRF Defenses
Manual Testing with corsproxy.io
One of the best ways to test your CSRF defenses is to use a tool like corsproxy.io to isolate CORS from CSRF:
// Step 1: Test direct API call
async function testDirect() {
const response = await fetch('https://your-api.com/sensitive-action', {
method: 'POST',
credentials: 'include',
body: JSON.stringify({ action: 'delete' })
});
console.log('Direct:', response.status);
}
// Step 2: Test through CORS proxy (bypasses CORS)
async function testThroughProxy() {
const response = await fetch('https://corsproxy.io/?url=https://your-api.com/sensitive-action', {
method: 'POST',
credentials: 'include',
body: JSON.stringify({ action: 'delete' })
});
console.log('Proxy:', response.status);
}
// Step 3: Analyze results
// Both should return 403 if CSRF protection is working
// If proxy works (200) but direct fails = CORS issue
// If both fail (403) = CSRF protection working correctly
Create a Test HTML Page
<!DOCTYPE html>
<html>
<body>
<h1>CSRF Test Page</h1>
<h2>Test 1: Form Submission</h2>
<form action="https://target-site.com/api/critical-action" method="POST">
<input type="hidden" name="action" value="malicious">
<button type="submit">Test Form CSRF</button>
</form>
<h2>Test 2: Fetch with Credentials</h2>
<button onclick="testFetch()">Test Fetch CSRF</button>
<h2>Test 3: Through Proxy</h2>
<button onclick="testProxy()">Test via corsproxy.io</button>
<div id="results"></div>
<script>
async function testFetch() {
try {
const response = await fetch('https://target-site.com/api/critical-action', {
method: 'POST',
credentials: 'include',
body: JSON.stringify({ action: 'test' })
});
document.getElementById('results').innerHTML = `Fetch: ${response.status}`;
} catch (e) {
document.getElementById('results').innerHTML = `Fetch Error: ${e.message}`;
}
}
async function testProxy() {
try {
const response = await fetch('https://corsproxy.io/?url=https://target-site.com/api/critical-action', {
method: 'POST',
credentials: 'include',
body: JSON.stringify({ action: 'test' })
});
document.getElementById('results').innerHTML += `<br>Proxy: ${response.status}`;
} catch (e) {
document.getElementById('results').innerHTML += `<br>Proxy Error: ${e.message}`;
}
}
</script>
</body>
</html>
Automated Testing Tools
- Burp Suite: Professional web security testing
- OWASP ZAP: Free, open-source security scanner
- CSRFire (Browser Extension): Quick CSRF testing
- Custom Scripts: Automated endpoint testing
Penetration Testing Checklist
- Test all state-changing endpoints without CSRF tokens
- Verify CSRF tokens are cryptographically random
- Check if tokens are tied to user sessions
- Test token reuse across sessions
- Verify SameSite cookie attributes
- Test cross-subdomain token validity
- Check file upload endpoint protection
- Verify API endpoint protection
- Test GET requests don’t change state
- Verify token expiration and renewal
- Use CORS proxy to isolate CORS vs CSRF issues
Conclusion
CSRF protection is not optional—it’s a critical security requirement for any web application that uses cookie-based authentication. The most common mistakes stem from misunderstanding how browsers handle cookies and confusing CSRF with other security mechanisms like CORS.
Key Takeaways:
-
CORS ≠ CSRF Protection: They serve completely different purposes. CORS controls who can read responses; CSRF prevents unauthorized state changes.
-
Use CSRF Tokens: Implement cryptographically secure, session-specific tokens for all state-changing operations.
-
SameSite Cookies: Use as defense-in-depth, not sole protection. Set
SameSite=LaxorStricton all session cookies. -
HTTP Methods Matter: Never use GET for state-changing operations. Always use POST, PUT, DELETE, or PATCH.
-
Protect Everything: APIs, file uploads, and all endpoints that modify server state need CSRF protection when using cookie-based auth.
-
Use Established Libraries: Don’t reinvent the wheel. Use your framework’s built-in CSRF protection.
-
Test Regularly: Include CSRF testing in your security workflow. Use tools like corsproxy.io to isolate and diagnose CORS vs CSRF issues during development.
-
Educate Your Team: Make sure all developers understand the difference between CORS and CSRF, and why both matter.
Need Help Debugging CORS vs CSRF Issues?
During development, distinguishing between CORS and CSRF errors can be challenging. corsproxy.io helps you quickly isolate the issue:
- Free tier: 5,000 requests/month
- No setup required: Just prepend
https://corsproxy.io/?url=to your API endpoint - Instant debugging: Determine if errors are CORS or CSRF related in seconds
- Global infrastructure: Low-latency proxy servers worldwide
Get started now: corsproxy.io
By avoiding these common mistakes and following best practices, you can build robust CSRF defenses that protect your users from attack. Remember: security is not a one-time effort but an ongoing process of vigilance, education, and improvement.
Protecting against CSRF is just one part of web security. Make sure to also implement proper authentication, authorization, input validation, and follow OWASP’s security guidelines for comprehensive application security.