This guide helps you update existing client-side code to work with CSRF protection.
As of this update, all state-changing API requests (POST, PUT, DELETE, PATCH) now require a CSRF token. This prevents cross-site request forgery attacks on payment and user management endpoints.
// Old code - NO CSRF protection
const response = await fetch('/api/checkout/adspace', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(checkoutData),
})// New code - WITH CSRF protection
import { fetchWithCSRF } from '@/lib/security/csrf-client'
const response = await fetchWithCSRF('/api/checkout/adspace', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(checkoutData),
})Search your codebase for state-changing fetch calls:
# Find POST requests
grep -r "method: 'POST'" --include="*.ts" --include="*.tsx"
# Find PUT requests
grep -r "method: 'PUT'" --include="*.ts" --include="*.tsx"
# Find DELETE requests
grep -r "method: 'DELETE'" --include="*.ts" --include="*.tsx"Add the import at the top of files with state-changing requests:
import { fetchWithCSRF } from '@/lib/security/csrf-client'Replace fetch( with fetchWithCSRF( for all state-changing requests:
Before:
const response = await fetch('/api/user/preferences', {
method: 'POST',
body: JSON.stringify(preferences),
})After:
const response = await fetchWithCSRF('/api/user/preferences', {
method: 'POST',
body: JSON.stringify(preferences),
})- Clear browser cookies
- Refresh the page to get a new CSRF token
- Test the state-changing operation
- Verify it works without CSRF errors
Before:
async function handleSubmit(e: FormEvent) {
e.preventDefault()
const data = new FormData(e.target)
const response = await fetch('/api/billing/subscription/update', {
method: 'POST',
body: JSON.stringify(Object.fromEntries(data)),
})
}After:
import { fetchWithCSRF } from '@/lib/security/csrf-client'
async function handleSubmit(e: FormEvent) {
e.preventDefault()
const data = new FormData(e.target)
const response = await fetchWithCSRF('/api/billing/subscription/update', {
method: 'POST',
body: JSON.stringify(Object.fromEntries(data)),
})
}Before:
async function handleDelete(id: string) {
const response = await fetch(`/api/user/saved-searches/${id}`, {
method: 'DELETE',
})
}After:
import { fetchWithCSRF } from '@/lib/security/csrf-client'
async function handleDelete(id: string) {
const response = await fetchWithCSRF(`/api/user/saved-searches/${id}`, {
method: 'DELETE',
})
}Before:
async function updateProfile(profile: Profile) {
const response = await fetch('/api/user/preferences', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(profile),
})
}After:
import { fetchWithCSRF } from '@/lib/security/csrf-client'
async function updateProfile(profile: Profile) {
const response = await fetchWithCSRF('/api/user/preferences', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(profile),
})
}Before:
'use client'
import { useState } from 'react'
export function CheckoutForm() {
const [loading, setLoading] = useState(false)
async function handleCheckout() {
setLoading(true)
try {
const response = await fetch('/api/checkout/adspace', {
method: 'POST',
body: JSON.stringify({ /* data */ }),
})
const result = await response.json()
// Handle result
} finally {
setLoading(false)
}
}
return <button onClick={handleCheckout}>Checkout</button>
}After:
'use client'
import { useState } from 'react'
import { fetchWithCSRF } from '@/lib/security/csrf-client'
export function CheckoutForm() {
const [loading, setLoading] = useState(false)
async function handleCheckout() {
setLoading(true)
try {
const response = await fetchWithCSRF('/api/checkout/adspace', {
method: 'POST',
body: JSON.stringify({ /* data */ }),
})
const result = await response.json()
// Handle result
} finally {
setLoading(false)
}
}
return <button onClick={handleCheckout}>Checkout</button>
}If you have a custom fetch wrapper, you can integrate CSRF manually:
import { getCSRFHeaders } from '@/lib/security/csrf-client'
async function apiClient(url: string, options: RequestInit) {
const headers = getCSRFHeaders(options.headers)
return fetch(url, {
...options,
headers,
})
}If you need direct access to the token in a React component:
'use client'
import { useCSRFToken } from '@/lib/security/csrf-client'
export function MyComponent() {
const csrfToken = useCSRFToken()
// Token is automatically updated when it changes
console.log('Current CSRF token:', csrfToken)
return <div>Token loaded: {csrfToken ? 'Yes' : 'No'}</div>
}If you need to ensure a token exists before making a request:
import { waitForCSRFToken, fetchWithCSRF } from '@/lib/security/csrf-client'
async function criticalOperation() {
// Wait up to 5 seconds for token
try {
await waitForCSRFToken(5000)
} catch (err) {
console.error('CSRF token not available')
return
}
// Token is now guaranteed to exist
await fetchWithCSRF('/api/billing/subscription/cancel', {
method: 'POST',
})
}Some routes are exempt from CSRF protection and don't need updates:
// No changes needed - webhooks use signature verification
export async function POST(request: Request) {
// Validate Stripe signature
const sig = request.headers.get('stripe-signature')
// ... webhook logic
}// No changes needed - cron uses CRON_SECRET
export async function GET(request: NextRequest) {
const auth = requireApiKey(request, { allow: ['CRON_SECRET'] })
// ... cron logic
}// No changes needed - API key provides authentication
export async function POST(request: NextRequest) {
const auth = requireApiKey(request, { allow: ['ADMIN_API_KEY'] })
// ... admin logic
}Cause: Missing or invalid CSRF token
Solution:
- Verify you're using
fetchWithCSRFinstead offetch - Check browser cookies for
csrf-token - Refresh the page to get a new token
- Check browser console for CSRF warnings
Cause: Token cookie not set by server
Solution:
- Ensure middleware is running (check Network tab)
- Clear all cookies and refresh
- Verify middleware.ts is properly configured
- Check that request goes through middleware
Cause: Cookie not being sent with request
Solution:
- Add
credentials: 'include'to fetch options - Verify same-origin request (CORS may block)
- Check that cookie domain matches request domain
After migration, verify:
- All state-changing requests include CSRF token
- Forms submit successfully
- Delete operations work
- Update operations work
- Error handling works correctly
- CSRF token refreshes after expiration
- No console warnings about missing tokens
If you need to temporarily disable CSRF protection for a specific route:
- Add route pattern to exempt list in
/lib/security/csrf.ts:
const CSRF_EXEMPT_PATTERNS = [
/^\/api\/webhooks\//,
/^\/api\/cron\//,
/^\/api\/your-route\//, // Add your route here
]- Document why it's exempt
- Add alternative protection (API key, signature verification)
- Create ticket to fix properly
See full documentation: docs/CSRF_PROTECTION.md
For security concerns, contact the security team.