403 Forbidden Due to CSRF Token Mismatch
Sintomas
- Form submissions return 403 Forbidden with a message like "CSRF verification failed. Request aborted." or "Invalid CSRF token"
- The form page loads and displays correctly, but all POST requests are rejected regardless of the form data
- The error appears after leaving the browser tab open for a long period (session or token expiry) then submitting
- AJAX fetch or XHR POST requests from JavaScript fail with 403 even though GET requests to the same domain work fine
- Clearing cookies and logging in again temporarily resolves the issue until the session or token expires again
- The form page loads and displays correctly, but all POST requests are rejected regardless of the form data
- The error appears after leaving the browser tab open for a long period (session or token expiry) then submitting
- AJAX fetch or XHR POST requests from JavaScript fail with 403 even though GET requests to the same domain work fine
- Clearing cookies and logging in again temporarily resolves the issue until the session or token expires again
Causas raiz
- CSRF token not included in the AJAX request headers — JavaScript fetch calls omit the `X-CSRFToken` or equivalent header
- Session expired on the server, invalidating the associated CSRF token that was embedded in the HTML form when the page was first rendered
- Domain mismatch between where the token was issued (e.g., example.com) and where the form submits (e.g., api.example.com) — treated as cross-site
- SameSite=Strict or SameSite=Lax cookie attribute preventing the CSRF cookie from being sent on cross-origin form submissions or redirects
- Caching layer (CDN, reverse proxy, or browser) serving a stale page with an already-expired CSRF token embedded in the HTML
Diagnóstico
**Step 1 — Check the request headers in browser DevTools**
Open Network tab → submit the form → click the failing request → inspect Request Headers:
```
# Good: token is present
X-CSRFToken: abc123...
Cookie: csrftoken=abc123...
# Bad: token missing
# No X-CSRFToken header at all
```
**Step 2 — Verify the token matches between cookie and header/form field**
The value in the `csrftoken` cookie must match the value sent in the request. A mismatch means the session expired or the page is cached:
```javascript
// In browser console
document.cookie.split(';').find(c => c.includes('csrftoken'))
// Compare with the form field value:
document.querySelector('[name=csrfmiddlewaretoken]').value
```
**Step 3 — Test with curl to confirm the server-side check**
```bash
# Fetch the page to get a fresh CSRF token
TOKEN=$(curl -c /tmp/cookies.txt https://example.com/form/ \
| grep -o 'csrfmiddlewaretoken.*value="[^"]*"' \
| grep -o 'value="[^"]*"' | cut -d'"' -f2)
# Submit with matching token and cookie
curl -b /tmp/cookies.txt -X POST https://example.com/submit/ \
-H "X-CSRFToken: $TOKEN" \
-d 'field=value'
```
**Step 4 — Check Django CSRF_TRUSTED_ORIGINS setting (Django 4.0+)**
```python
# settings.py — must include all origins that submit forms
CSRF_TRUSTED_ORIGINS = [
'https://example.com',
'https://www.example.com',
'https://api.example.com', # If submitting cross-subdomain
]
```
**Step 5 — Inspect CSRF cookie attributes**
```bash
curl -I https://example.com/
# Look for Set-Cookie header:
# Set-Cookie: csrftoken=abc; SameSite=Lax; Secure
# SameSite=Strict will block cross-site form submissions
```
Open Network tab → submit the form → click the failing request → inspect Request Headers:
```
# Good: token is present
X-CSRFToken: abc123...
Cookie: csrftoken=abc123...
# Bad: token missing
# No X-CSRFToken header at all
```
**Step 2 — Verify the token matches between cookie and header/form field**
The value in the `csrftoken` cookie must match the value sent in the request. A mismatch means the session expired or the page is cached:
```javascript
// In browser console
document.cookie.split(';').find(c => c.includes('csrftoken'))
// Compare with the form field value:
document.querySelector('[name=csrfmiddlewaretoken]').value
```
**Step 3 — Test with curl to confirm the server-side check**
```bash
# Fetch the page to get a fresh CSRF token
TOKEN=$(curl -c /tmp/cookies.txt https://example.com/form/ \
| grep -o 'csrfmiddlewaretoken.*value="[^"]*"' \
| grep -o 'value="[^"]*"' | cut -d'"' -f2)
# Submit with matching token and cookie
curl -b /tmp/cookies.txt -X POST https://example.com/submit/ \
-H "X-CSRFToken: $TOKEN" \
-d 'field=value'
```
**Step 4 — Check Django CSRF_TRUSTED_ORIGINS setting (Django 4.0+)**
```python
# settings.py — must include all origins that submit forms
CSRF_TRUSTED_ORIGINS = [
'https://example.com',
'https://www.example.com',
'https://api.example.com', # If submitting cross-subdomain
]
```
**Step 5 — Inspect CSRF cookie attributes**
```bash
curl -I https://example.com/
# Look for Set-Cookie header:
# Set-Cookie: csrftoken=abc; SameSite=Lax; Secure
# SameSite=Strict will block cross-site form submissions
```
Resolução
**Fix 1: Include CSRF token in AJAX requests (Django)**
```javascript
// Read the token from the cookie
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
}
const csrfToken = getCookie('csrftoken');
// Include in every mutating request
await fetch('/api/submit/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken, // Required by Django
},
body: JSON.stringify(data),
});
```
**Fix 2: Set up CSRF for all axios requests globally**
```javascript
import axios from 'axios';
// Configure once at app startup
axios.defaults.xsrfCookieName = 'csrftoken'; // Django default
axios.defaults.xsrfHeaderName = 'X-CSRFToken';
axios.defaults.withCredentials = true;
```
**Fix 3: For SPAs — use CSRF_COOKIE_SAMESITE = 'Lax' and ensure cookies are sent**
```python
# settings.py
CSRF_COOKIE_SAMESITE = 'Lax' # Default; use 'None' for cross-site (requires HTTPS)
CSRF_COOKIE_SECURE = True # Required when SameSite=None
CSRF_TRUSTED_ORIGINS = ['https://app.example.com']
SESSION_COOKIE_SAMESITE = 'Lax'
```
**Fix 4: Extend session lifetime to reduce token expiry issues**
```python
# settings.py
SESSION_COOKIE_AGE = 86400 * 7 # 7 days (default: 2 weeks)
SESSION_EXPIRE_AT_BROWSER_CLOSE = False
```
**Fix 5: For API-only endpoints that don't need CSRF (use authentication instead)**
```python
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
@method_decorator(csrf_exempt, name='dispatch')
class MyAPIView(View):
# Only exempt if you're using JWT/API key auth instead
pass
```
```javascript
// Read the token from the cookie
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
}
const csrfToken = getCookie('csrftoken');
// Include in every mutating request
await fetch('/api/submit/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken, // Required by Django
},
body: JSON.stringify(data),
});
```
**Fix 2: Set up CSRF for all axios requests globally**
```javascript
import axios from 'axios';
// Configure once at app startup
axios.defaults.xsrfCookieName = 'csrftoken'; // Django default
axios.defaults.xsrfHeaderName = 'X-CSRFToken';
axios.defaults.withCredentials = true;
```
**Fix 3: For SPAs — use CSRF_COOKIE_SAMESITE = 'Lax' and ensure cookies are sent**
```python
# settings.py
CSRF_COOKIE_SAMESITE = 'Lax' # Default; use 'None' for cross-site (requires HTTPS)
CSRF_COOKIE_SECURE = True # Required when SameSite=None
CSRF_TRUSTED_ORIGINS = ['https://app.example.com']
SESSION_COOKIE_SAMESITE = 'Lax'
```
**Fix 4: Extend session lifetime to reduce token expiry issues**
```python
# settings.py
SESSION_COOKIE_AGE = 86400 * 7 # 7 days (default: 2 weeks)
SESSION_EXPIRE_AT_BROWSER_CLOSE = False
```
**Fix 5: For API-only endpoints that don't need CSRF (use authentication instead)**
```python
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
@method_decorator(csrf_exempt, name='dispatch')
class MyAPIView(View):
# Only exempt if you're using JWT/API key auth instead
pass
```
Prevenção
- **Use token-based auth (JWT) for pure API endpoints** — CSRF protection is for cookie-based session auth; REST APIs using `Authorization: Bearer` headers are not vulnerable to CSRF attacks
- **Set Cache-Control: no-store on pages with CSRF forms** — this prevents shared caches from serving stale pages with expired tokens to different users
- **Configure CSRF_TRUSTED_ORIGINS on day one** — adding every origin that legitimately submits forms prevents mismatch errors during cross-subdomain deployments
- **Test CSRF flows in staging** by simulating session expiry: load the form, expire the session manually, then submit to confirm the error message is helpful and the token refresh works
- **Set Cache-Control: no-store on pages with CSRF forms** — this prevents shared caches from serving stale pages with expired tokens to different users
- **Configure CSRF_TRUSTED_ORIGINS on day one** — adding every origin that legitimately submits forms prevents mismatch errors during cross-subdomain deployments
- **Test CSRF flows in staging** by simulating session expiry: load the form, expire the session manually, then submit to confirm the error message is helpful and the token refresh works