JudgeFinder Platform implements CSRF (Cross-Site Request Forgery) protection using the double-submit cookie pattern to prevent unauthorized state-changing operations.
CSRF attacks trick authenticated users into executing unwanted actions by exploiting their active session. Without CSRF protection, an attacker could create a malicious website that submits forms to JudgeFinder on behalf of logged-in users.
We use the double-submit cookie pattern:
- Server generates a cryptographically secure token
- Token is sent to client in both:
- HTTP cookie (
csrf-token) - Response headers (for debugging)
- HTTP cookie (
- Client includes token in
X-CSRF-Tokenheader on state-changing requests - Server validates that cookie token matches header token
This works because:
- Cookies are sent automatically by the browser
- Headers must be set explicitly by JavaScript
- Malicious sites can trigger requests but cannot read/set custom headers due to CORS
All state-changing HTTP methods are protected:
POSTPUTDELETEPATCH
Safe methods are NOT protected (no CSRF risk):
GETHEADOPTIONS
/api/checkout/* - Payment processing
/api/billing/* - Subscription management
/api/user/* - User settings
/api/admin/* - Admin operations (also has API key auth)
The following routes are exempt from CSRF protection:
-
Webhooks (
/api/webhooks/*)- Use cryptographic signature verification instead
- Stripe, Coinbase, etc. webhooks
-
Cron Jobs (
/api/cron/*)- Protected by
CRON_SECRETAPI key - Called by Netlify scheduler, not browsers
- Protected by
-
API Key Authenticated Routes (
/api/admin/*)- When request includes valid
X-API-Keyheader - CSRF not applicable for non-browser clients
- When request includes valid
Use the fetchWithCSRF wrapper for all state-changing requests:
import { fetchWithCSRF } from '@/lib/security/csrf-client'
// Automatically includes CSRF token
const response = await fetchWithCSRF('/api/checkout/adspace', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})For custom fetch implementations:
import { getCSRFToken } from '@/lib/security/csrf-client'
const token = getCSRFToken()
const response = await fetch('/api/billing/subscription/cancel', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': token || '',
},
body: JSON.stringify({ reason: 'user_request' }),
})For React components that need token access:
'use client'
import { useCSRFToken } from '@/lib/security/csrf-client'
export function MyComponent() {
const csrfToken = useCSRFToken()
const handleSubmit = async () => {
if (!csrfToken) {
console.error('CSRF token not available')
return
}
// Use token in request...
}
}CSRF protection is enforced in /middleware.ts:
// Runs before all routes
const csrfError = enforceCSRFProtection(request)
if (csrfError) {
return csrfError // 403 Forbidden
}For additional validation in specific routes:
import { validateCSRFToken } from '@/lib/security/csrf'
export async function POST(request: NextRequest) {
// Validate CSRF token
if (!validateCSRFToken(request)) {
return NextResponse.json({ error: 'CSRF validation failed' }, { status: 403 })
}
// Process request...
}To exempt a route from CSRF protection, add pattern to /lib/security/csrf.ts:
const CSRF_EXEMPT_PATTERNS = [/^\/api\/webhooks\//, /^\/api\/cron\//, /^\/api\/your-exempt-route\//]- Algorithm: Web Crypto API
crypto.getRandomValues() - Length: 32 bytes (64 hex characters)
- Entropy: 256 bits of cryptographic randomness
- Cookie Name:
csrf-token - HttpOnly:
false(must be readable by JavaScript) - Secure:
true(production only, HTTPS required) - SameSite:
strict(maximum CSRF protection) - Max-Age: 24 hours
Tokens expire after 24 hours. Middleware automatically:
- Checks for existing valid token
- Generates new token if missing or expired
- Sets cookie on all responses
{
"error": "CSRF validation failed",
"code": "CSRF_TOKEN_INVALID",
"message": "Invalid or missing CSRF token. Please refresh the page and try again."
}Status Code: 403 Forbidden
{
"error": "CSRF token not found",
"code": "CSRF_TOKEN_NOT_FOUND",
"message": "No CSRF token found in cookies. Please refresh the page."
}Status Code: 404 Not Found
Token validation uses constant-time string comparison to prevent timing attacks:
function constantTimeEqual(a: string, b: string): boolean {
let result = 0
for (let i = 0; i < a.length; i++) {
result |= a.charCodeAt(i) ^ b.charCodeAt(i)
}
return result === 0
}CSRF protection is one layer in our security model:
- CSRF Tokens - Prevent unauthorized state changes
- SameSite Cookies - Block cross-site cookie transmission
- CORS Headers - Restrict cross-origin requests
- Authentication - Verify user identity (Clerk)
- Authorization - Verify user permissions (RBAC)
- Rate Limiting - Prevent abuse (Upstash Redis)
- Sub-domain Attacks:
SameSite=strictprevents attacks from subdomains - XSS Vulnerabilities: If XSS exists, attacker can read token. Mitigate with CSP.
- Man-in-the-Middle: Requires HTTPS in production (enforced)
-
GET Request (should succeed):
curl https://judgefinder.io/api/judges/list
-
POST without token (should fail with 403):
curl -X POST https://judgefinder.io/api/checkout/adspace \ -H "Content-Type: application/json" \ -d '{"data":"test"}'
-
POST with token (should succeed):
# Get token from browser cookies TOKEN="your-csrf-token-here" curl -X POST https://judgefinder.io/api/checkout/adspace \ -H "Content-Type: application/json" \ -H "X-CSRF-Token: $TOKEN" \ -H "Cookie: csrf-token=$TOKEN" \ -d '{"data":"test"}'
import { validateCSRFToken, generateCSRFToken } from '@/lib/security/csrf'
describe('CSRF Protection', () => {
it('should reject requests without CSRF token', () => {
const request = new Request('https://example.com/api/test', {
method: 'POST',
})
expect(validateCSRFToken(request)).toBe(false)
})
it('should accept requests with valid CSRF token', () => {
const token = generateCSRFToken()
const request = new Request('https://example.com/api/test', {
method: 'POST',
headers: {
'X-CSRF-Token': token,
Cookie: `csrf-token=${token}`,
},
})
expect(validateCSRFToken(request)).toBe(true)
})
})- Check that client is sending
X-CSRF-Tokenheader - Verify cookie is set: Open DevTools → Application → Cookies →
csrf-token - Ensure request includes credentials:
credentials: 'include'in fetch - Check CORS headers allow custom headers
- Verify middleware is running: Check Network tab for CSRF cookie
- Clear browser cookies and refresh
- Check that
NODE_ENVis set correctly
- Verify route is in
CSRF_EXEMPT_PATTERNS - Check webhook uses signature verification instead
- Ensure webhook doesn't include CSRF token accidentally
This implementation satisfies:
- OWASP Top 10 - A01:2021 Broken Access Control
- OWASP CSRF Prevention Cheat Sheet - Double Submit Cookie Pattern
- PCI DSS 6.5.9 - Protection against CSRF attacks
- NIST SP 800-63B - Session management best practices