403 Forbidden Due to CORS Misconfiguration
Symptômes
- Browser console shows `Access to fetch at 'https://api.example.com' from origin 'https://app.example.com' has been blocked by CORS policy`
- Network devtools shows an OPTIONS preflight request returning 403 or 404 before the actual POST/PUT is attempted
- The request works perfectly in `curl` or Postman but fails in the browser — CORS is browser-enforced, not server-enforced
- Response headers are missing `Access-Control-Allow-Origin` entirely, or it says `*` but the request includes `credentials: 'include'`
- Error only appears on non-simple requests (POST with JSON body, custom headers like `Authorization`) — simple GET requests pass
- Network devtools shows an OPTIONS preflight request returning 403 or 404 before the actual POST/PUT is attempted
- The request works perfectly in `curl` or Postman but fails in the browser — CORS is browser-enforced, not server-enforced
- Response headers are missing `Access-Control-Allow-Origin` entirely, or it says `*` but the request includes `credentials: 'include'`
- Error only appears on non-simple requests (POST with JSON body, custom headers like `Authorization`) — simple GET requests pass
Causes profondes
- Missing or incorrect `Access-Control-Allow-Origin` header — the server does not include the requesting origin in its CORS allowlist
- Preflight OPTIONS request is not handled — the server returns 403 or 404 for OPTIONS instead of 200 with the appropriate CORS headers
- `Access-Control-Allow-Origin: *` combined with `Access-Control-Allow-Credentials: true` — the spec forbids this combination; browsers reject it
- Request sends a custom header (e.g. `Authorization`, `X-API-Key`) that is not listed in `Access-Control-Allow-Headers`
- HTTP method (e.g. PATCH, DELETE) is not listed in `Access-Control-Allow-Methods`, so the preflight rejects the request
Diagnostic
**Step 1: Read the exact browser error message**
```
Open DevTools → Console tab
The message names the exact missing header or policy violation
```
**Step 2: Inspect the preflight OPTIONS request**
```
DevTools → Network tab → filter 'OPTIONS'
Check Response Headers for:
Access-Control-Allow-Origin
Access-Control-Allow-Methods
Access-Control-Allow-Headers
Access-Control-Allow-Credentials
```
**Step 3: Reproduce with curl**
```bash
# Simulate the preflight
curl -X OPTIONS https://api.example.com/data \
-H 'Origin: https://app.example.com' \
-H 'Access-Control-Request-Method: POST' \
-H 'Access-Control-Request-Headers: Authorization,Content-Type' \
-v 2>&1 | grep -i 'access-control'
```
**Step 4: Check server-side CORS middleware config**
```python
# Django: check CORS_ALLOWED_ORIGINS in settings.py
from django.conf import settings
print(settings.CORS_ALLOWED_ORIGINS)
print(settings.CORS_ALLOW_CREDENTIALS)
```
**Step 5: Use the CORS Checker tool**
Paste your API URL and origin into the CORS Checker tool above to see exactly which headers are present and which are missing.
```
Open DevTools → Console tab
The message names the exact missing header or policy violation
```
**Step 2: Inspect the preflight OPTIONS request**
```
DevTools → Network tab → filter 'OPTIONS'
Check Response Headers for:
Access-Control-Allow-Origin
Access-Control-Allow-Methods
Access-Control-Allow-Headers
Access-Control-Allow-Credentials
```
**Step 3: Reproduce with curl**
```bash
# Simulate the preflight
curl -X OPTIONS https://api.example.com/data \
-H 'Origin: https://app.example.com' \
-H 'Access-Control-Request-Method: POST' \
-H 'Access-Control-Request-Headers: Authorization,Content-Type' \
-v 2>&1 | grep -i 'access-control'
```
**Step 4: Check server-side CORS middleware config**
```python
# Django: check CORS_ALLOWED_ORIGINS in settings.py
from django.conf import settings
print(settings.CORS_ALLOWED_ORIGINS)
print(settings.CORS_ALLOW_CREDENTIALS)
```
**Step 5: Use the CORS Checker tool**
Paste your API URL and origin into the CORS Checker tool above to see exactly which headers are present and which are missing.
Résolution
**Fix 1: Django — correct django-cors-headers config**
```python
# settings.py
INSTALLED_APPS = [
...
'corsheaders',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware', # must be FIRST
'django.middleware.common.CommonMiddleware',
...
]
CORS_ALLOWED_ORIGINS = [
'https://app.example.com',
'https://www.example.com',
]
CORS_ALLOW_CREDENTIALS = True # only with specific origins, not '*'
CORS_ALLOW_HEADERS = [
'authorization',
'content-type',
'x-api-key',
]
```
**Fix 2: Nginx — add CORS headers for all OPTIONS**
```nginx
location /api/ {
if ($request_method = OPTIONS) {
add_header 'Access-Control-Allow-Origin' 'https://app.example.com';
add_header 'Access-Control-Allow-Methods' 'GET,POST,PUT,PATCH,DELETE,OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Authorization,Content-Type';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Max-Age' 86400;
return 204;
}
proxy_pass http://backend;
}
```
**Fix 3: Node.js/Express — cors package**
```javascript
import cors from 'cors';
app.use(cors({
origin: ['https://app.example.com'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Authorization', 'Content-Type'],
}));
```
```python
# settings.py
INSTALLED_APPS = [
...
'corsheaders',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware', # must be FIRST
'django.middleware.common.CommonMiddleware',
...
]
CORS_ALLOWED_ORIGINS = [
'https://app.example.com',
'https://www.example.com',
]
CORS_ALLOW_CREDENTIALS = True # only with specific origins, not '*'
CORS_ALLOW_HEADERS = [
'authorization',
'content-type',
'x-api-key',
]
```
**Fix 2: Nginx — add CORS headers for all OPTIONS**
```nginx
location /api/ {
if ($request_method = OPTIONS) {
add_header 'Access-Control-Allow-Origin' 'https://app.example.com';
add_header 'Access-Control-Allow-Methods' 'GET,POST,PUT,PATCH,DELETE,OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Authorization,Content-Type';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Max-Age' 86400;
return 204;
}
proxy_pass http://backend;
}
```
**Fix 3: Node.js/Express — cors package**
```javascript
import cors from 'cors';
app.use(cors({
origin: ['https://app.example.com'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Authorization', 'Content-Type'],
}));
```
Prévention
- **Never use `*` with credentials**: If your requests include cookies or `Authorization` headers, you must list specific origins in `Access-Control-Allow-Origin` — the wildcard is prohibited by spec
- **Environment-specific origin lists**: Use separate CORS allowlists for development (`http://localhost:3000`) and production (`https://app.example.com`) via environment variables
- **Cache preflight**: Return `Access-Control-Max-Age: 86400` (24h) so browsers don't fire an OPTIONS preflight on every request
- **Integration tests**: Add a test that sends an OPTIONS preflight to your API and asserts all required CORS headers are present
- **Document your CORS policy**: List allowed origins and methods in your API docs — frontend teams shouldn't discover restrictions at runtime
- **Environment-specific origin lists**: Use separate CORS allowlists for development (`http://localhost:3000`) and production (`https://app.example.com`) via environment variables
- **Cache preflight**: Return `Access-Control-Max-Age: 86400` (24h) so browsers don't fire an OPTIONS preflight on every request
- **Integration tests**: Add a test that sends an OPTIONS preflight to your API and asserts all required CORS headers are present
- **Document your CORS policy**: List allowed origins and methods in your API docs — frontend teams shouldn't discover restrictions at runtime