đ django-cors-headers â CORS propre et sĂ©curisĂ© sur Django
Guide IDEOâLab ultra-pratique : installation, architecture CORS, config fine, piĂšges sĂ©curitĂ©, DRF, prĂ©flight, Nginx, et recettes prod.
Pourquoi CORS existe (vraiment)
SameâOrigin Policy, navigateur, Origin, headers AccessâControlâ*, prĂ©flight OPTIONS.
Architecture (mental model)
Qui parle Ă qui ? Frontend â navigateur â Django. CORS n'est pas âun truc serveurâ, c'est une rĂšgle navigateur.
Request FlowHeadersPolicyInstallation & intégration Django
pip install, INSTALLED_APPS, ordre MIDDLEWARE, check rapide.
Quickstart (dev/prod)
Config minimale propre, puis config âprodâ restrictive + credentials + headers custom.
MinimalProductionChecklistSettings essentiels
CORS_ALLOWED_ORIGINS, regex, allowâall, methods, headers, expose, credentials.
Préflight & performance
Quand un OPTIONS apparaĂźt, Access-Control-Request-*, cache preflight, timeouts.
SĂ©curitĂ© : CORS â Auth â CSRF
Erreurs classiques : * + credentials, confusion avec CSRF, fuite de cookies, âopen APIâ.
DRF / JWT / Cookies
API Django REST : tokens vs cookies, Authorization header, withCredentials, SameSite.
Nginx / Reverse proxy
OĂč mettre CORS ? (souvent dans Django), mais parfois au proxy. Attention doubles headers / incohĂ©rences.
NginxProxyHeadersTests & vérification
curl, DevTools, reproduire un préflight, tests Django/pytest, validation CI.
curlDevToolspytestRecettes (réelles) de prod
Multiâenv, multiâfrontends, sousâdomaines, preview deployments, API publique + backoffice.
PatternsMulti-envSaaSTroubleshooting
Les 12 erreurs les plus frĂ©quentes + diagnostic âoĂč ça casseâ + solutions.
DebugChecklistCommon ErrorsLiens & ressources
Docs officielles, spec CORS, articles, outils, snippets.
DocsSpecRefsNiveau & complexité
Quand câest simple, quand ça devient âpiĂ©geuxâ, et comment le rendre robuste.
DifficultyRiskOpsCheatâsheet
Config prĂȘtâĂ âcoller, commandes curl, checklists, matrices âfrontend â backendâ.
Copy/PastecurlRunbookSame-Origin Policy (SOP) : le âmurâ navigateur
Par dĂ©faut, un script JavaScript exĂ©cutĂ© sur https://app.example.com nâa pas le droit de lire la rĂ©ponse dâune requĂȘte faite vers https://api.example.com (origine diffĂ©rente). Câest la Same-Origin Policy.
scheme + host + portEx:
https://site.com â http://site.com â https://site.com:8443CORS : une permission contrĂŽlĂ©e par des headers
CORS est un mĂ©canisme standard du web oĂč le serveur indique au navigateur âtu peux autoriser ce JavaScript Ă lire cette rĂ©ponseâ. Le navigateur envoie un header Origin et le serveur rĂ©pond avec des headers Access-Control-Allow-*.
Headers qui comptent le plus
| Header | RĂŽle | Remarque |
|---|---|---|
Origin | EnvoyĂ© par le navigateur (source de la page) | PrĂ©sent surtout sur requĂȘtes âCORSâ. |
Access-Control-Allow-Origin | Autorise une origine (ou refuse) | Jamais * si credentials. |
Access-Control-Allow-Credentials | Autorise cookies / auth HTTP | Doit ĂȘtre true + origine explicite. |
Access-Control-Allow-Headers | Autorise headers non âsimplesâ | Ex: Authorization, X-Request-ID. |
Access-Control-Allow-Methods | Autorise méthodes HTTP | Important sur préflight. |
Point clĂ© : CORS nâest pas une auth
CORS ne âprotĂšgeâ pas vos endpoints contre un attaquant. Il contrĂŽle seulement si le navigateur laisse un script lire la rĂ©ponse. Un attaquant peut toujours appeler votre API depuis un serveur (curl, scripts) sans CORS.
Quand tu as besoin de django-cors-headers ?
- Ton frontend (React/Vue/Angular) est sur un autre domaine/port que ton backend Django.
- Tu utilises un API Gateway / domaine
api.*sĂ©parĂ©. - Tu sers lâAPI sur
localhost:8000et le frontend surlocalhost:5173. - Tu as un backoffice / admin sĂ©parĂ© dâun front grand public.
Le piĂšge n°1 : âça marche avec Postmanâ
Postman / curl ne sont pas soumis Ă la Same-Origin Policy. Donc lâAPI ârĂ©pondâ mais le navigateur bloque la lecture. RĂ©sultat : console âCORS errorâ alors que le backend fonctionne.
Le piÚge n°2 : préflight invisible
Tu vois ton POST Ă©chouer mais la vraie erreur est souvent sur le OPTIONS envoyĂ© juste avant. Regarde dans DevTools â Network â requĂȘte OPTIONS.
Ressources de référence
- Page PyPI (description) : pypi.org/project/django-cors-headers
- Repo GitHub : github.com/adamchainz/django-cors-headers
- README (raw) : README.rst
Schéma mental
[Frontend SPA] --fetch/axios--> [Navigateur]
| |
| (Origin: https://app...) | (applique SOP + CORS)
|------------------------------>|
|---- HTTP ----> [Django API]
| (répond avec Access-Control-*)
|<--- HTTP -----|
|<------ JS reçoit rĂ©ponse si CORS OK ---------|CORS est un contrat entre le navigateur et le serveur. Le backend âautoriseâ via headers, et le navigateur âfait respecterâ.
Simple request (pas de préflight)
Le navigateur peut envoyer directement la requĂȘte si elle respecte des critĂšres âsimplesâ (mĂ©thode, headers, content-type). Dans le doute, retiens surtout : JSON + headers custom dĂ©clenchent souvent un prĂ©flight.
GET https://api.example.com/v1/items Origin: https://app.example.com HTTP/200 Access-Control-Allow-Origin: https://app.example.com
PrĂ©flight (OPTIONS) : quand le navigateur âdemande permissionâ
OPTIONS /v1/items HTTP/1.1 Origin: https://app.example.com Access-Control-Request-Method: POST Access-Control-Request-Headers: content-type, authorization HTTP/204 Access-Control-Allow-Origin: https://app.example.com Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS Access-Control-Allow-Headers: content-type, authorization Access-Control-Max-Age: 86400 # Ensuite seulement, le navigateur envoie le POST réel.
Si le OPTIONS Ă©choue (403, 500, manque de headers), ton POST ne partira mĂȘme pas.
Position du middleware
django-cors-headers sâinsĂšre comme middleware Django : il inspecte Origin et ajoute les headers CORS sur la rĂ©ponse. Il gĂšre aussi correctement les rĂ©ponses de prĂ©flight.
CommonMiddleware), afin que les rĂ©ponses (y compris erreurs) soient âcorsifiĂ©esâ. îčPourquoi âavant CommonMiddlewareâ ?
CommonMiddlewarepeut rĂ©pondre tĂŽt (redirect slash, etc.).- Si la rĂ©ponse part sans headers CORS, le navigateur bloquera, et tu verras un âCORS errorâ au lieu du vrai statut.
pip
# Dans ton venv pip install django-cors-headers # (Optionnel) pin version dans requirements pip freeze | grep cors
Source : PyPI / repo officiel. îč
settings.py : INSTALLED_APPS + MIDDLEWARE
INSTALLED_APPS = [
# ...
"corsheaders",
# ...
]
MIDDLEWARE = [
"corsheaders.middleware.CorsMiddleware",
"django.middleware.common.CommonMiddleware", # CORS doit ĂȘtre avant CommonMiddleware
# ...
]DRF / API-only
Ăa marche pareil. Le point clĂ© est lâordre du middleware et la liste des origines.
Test rapide (curl)
curl -i https://api.example.com/health \ -H "Origin: https://app.example.com"
Tu dois voir dans la réponse :
Access-Control-Allow-Origin: https://app.example.com
Test préflight
curl -i -X OPTIONS https://api.example.com/v1/items \ -H "Origin: https://app.example.com" \ -H "Access-Control-Request-Method: POST" \ -H "Access-Control-Request-Headers: content-type, authorization"
Si tu ne vois pas Access-Control-Allow-Methods / Allow-Headers, ta config est incomplĂšte (ou le middleware trop bas).
Config DEV (rapide, contrÎlée)
Objectif : autoriser uniquement ton front local (Vite/Next) + éventuellement un domaine de staging.
# settings_dev.py
INSTALLED_APPS += ["corsheaders"]
MIDDLEWARE = ["corsheaders.middleware.CorsMiddleware"] + MIDDLEWARE
CORS_ALLOWED_ORIGINS = [
"http://localhost:5173", # Vite
"http://localhost:3000", # Next.js/React dev
]
# Souvent utile quand tu as Authorization: Bearer ...
CORS_ALLOW_HEADERS = [
"accept",
"authorization",
"content-type",
"origin",
"user-agent",
"x-csrftoken",
"x-requested-with",
"x-request-id",
]
# Si tu utilises cookies de session en dev (rare pour API token)
CORS_ALLOW_CREDENTIALS = TrueCORS_ALLOW_ALL_ORIGINS=True sauf pour un POC trĂšs court. Ăa masque des erreurs et tâemmĂšne vers une config prod dangereuse.Config PROD (restrictive, robuste)
Objectif : autoriser précisément tes origines frontend, gérer credentials si besoin, et expliciter ce qui est exposé.
# settings_prod.py
INSTALLED_APPS += ["corsheaders"]
MIDDLEWARE = ["corsheaders.middleware.CorsMiddleware"] + MIDDLEWARE
CORS_ALLOWED_ORIGINS = [
"https://app.example.com",
"https://admin.example.com",
]
# Autoriser cookies / auth HTTP seulement si nécessaire
CORS_ALLOW_CREDENTIALS = True
# Headers que le browser est autorisé à envoyer
CORS_ALLOW_HEADERS = [
"accept",
"authorization",
"content-type",
"origin",
"user-agent",
"x-csrftoken",
"x-requested-with",
"x-request-id",
]
# Headers que le browser a le droit de lire cÎté JS
CORS_EXPOSE_HEADERS = [
"content-disposition", # download filename
"x-request-id", # corr_id
]
# Cache preflight (perf)
CORS_PREFLIGHT_MAX_AGE = 86400 # 24hCORS_ALLOW_CREDENTIALS=True, le serveur doit répondre avec une origine explicite, jamais *.Origines : liste ou regex
Tu choisis qui a le droit de lire ta réponse cÎté navigateur.
| Setting | But | Exemple |
|---|---|---|
CORS_ALLOWED_ORIGINS | Liste explicite dâorigines autorisĂ©es | ["https://app.example.com"] |
CORS_ALLOWED_ORIGIN_REGEXES | Autoriser dynamiquement (preview env, sous-domaines) | [r"^https://.*\.preview\.example\.com$"] |
CORS_ALLOW_ALL_ORIGINS | Autoriser toutes les origines (dangereux) | True (à éviter en prod) |
# Exemple âpreview deploymentsâ (Netlify/Vercel-like) CORS_ALLOWED_ORIGIN_REGEXES = [ r"^https://[a-z0-9-]+--app\.example\.pages\.dev$", r"^https://pr-\d+\.preview\.example\.com$", ]
.*) revient Ă un allowâall dĂ©guisĂ©.MĂ©thodes & headers (prĂ©flight)
Le prĂ©flight vĂ©rifie si le browser peut envoyer certaines mĂ©thodes/headers. Si tu utilises Authorization, assure-toi quâil est autorisĂ©.
# Autoriser des mĂ©thodes (souvent inutile de surcharger) CORS_ALLOW_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"] # Autoriser des headers custom (la liste ci-dessous est un bon âbaselineâ) CORS_ALLOW_HEADERS = [ "accept", "authorization", "content-type", "origin", "user-agent", "x-csrftoken", "x-requested-with", "x-request-id", ]
Exposer des headers au JS
CORS_EXPOSE_HEADERS = [ "content-disposition", # download "x-request-id", # corr_id "x-rate-limit-remaining" ]
response.headers.get().Credentials (cookies / auth) : le point âdangereuxâ
CORS_ALLOW_CREDENTIALS permet au navigateur dâenvoyer des cookies et dâexposer la rĂ©ponse au JS quand le frontend appelle lâAPI avec credentials: "include" (fetch) ou withCredentials: true (axios).
# Django
CORS_ALLOW_CREDENTIALS = True
# fetch (frontend)
fetch("https://api.example.com/v1/me", {
method: "GET",
credentials: "include",
})
# axios (frontend)
axios.get("https://api.example.com/v1/me", { withCredentials: true })Access-Control-Allow-Origin: * + Access-Control-Allow-Credentials: true. Les navigateurs refusent par design. Et câest une mauvaise idĂ©e sĂ©curitĂ©.Cookies & SameSite (rappel)
Si tu relies CORS Ă des cookies de session, tu dois aussi gĂ©rer SameSite / Secure cĂŽtĂ© cookie. Sinon âça marche en localâ mais pas en HTTPS / cross-site.
Scopes / exclusions / hooks
Tu peux exclure des URLs (ex: admin) ou appliquer une logique plus fine via signaux. (Typiquement : autoriser certains origins uniquement pour certains chemins).
# Exemple de pattern (si disponible selon version): # CORS_URLS_REGEX = r"^/api/.*$" # Et pour des cas avancés, regarder le repo (signals / configuration).
Pour les options avancĂ©es exactes, rĂ©fĂšre-toi au README officiel (settings list) : îč README.rst
Quand un préflight est déclenché ?
Le navigateur dĂ©clenche souvent un OPTIONS avant la requĂȘte ârĂ©elleâ quand :
- MĂ©thode â GET/HEAD/POST âsimpleâ (PUT/PATCH/DELETEâŠ)
Content-Typenon âsimpleâ (souventapplication/json)- Headers custom (ex:
Authorization,X-Request-ID)
CORS_PREFLIGHT_MAX_AGE (cache).Cache preflight
# Django CORS_PREFLIGHT_MAX_AGE = 86400
Ce paramĂštre se traduit en Access-Control-Max-Age. Attention : certains navigateurs imposent des plafonds.
Préflight qui échoue : symptÎmes typiques
| SymptÎme | Cause fréquente | Fix |
|---|---|---|
| Console âCORS policyâŠâ et ton POST nâapparaĂźt pas | OPTIONS 403/404/500 | Mettre le middleware plus haut + autoriser headers/methods |
| OPTIONS 301/302 | Redirect slash / httpâhttps | CORS middleware avant CommonMiddleware + corriger URL |
| OPTIONS 405 Method not allowed | Endpoint ne gÚre pas OPTIONS | Middleware CORS doit répondre / config DRF |
| OK en dev, KO en prod | Nginx supprime headers / cache / CDN | Inspecter headers au niveau final (curl -I) |
Reproduire un préflight en curl
curl -i -X OPTIONS https://api.example.com/v1/items \ -H "Origin: https://app.example.com" \ -H "Access-Control-Request-Method: POST" \ -H "Access-Control-Request-Headers: content-type, authorization, x-request-id"
Clarification ultra importante
| Concept | ProtĂšge quoi ? | OĂč ? |
|---|---|---|
| CORS | EmpĂȘche un script browser non autorisĂ© de lire la rĂ©ponse | Navigateur (SOP) |
| AuthN/AuthZ | ContrĂŽle lâaccĂšs aux ressources | Serveur (Django) |
| CSRF | EmpĂȘche des actions non voulues via cookies (forgery) | Navigateur + serveur |
PiÚges sécurité (top)
ALLOW_ALLen prod : expose ton API au JS de nâimporte quel site (mauvais signe).- Cookies + CORS mal rĂ©glĂ© : fuite de session potentielle, confusion avec SameSite.
- Origines trop larges : regex permissive = attaque par sous-domaine contrÎlé.
- âCORS = firewallâ : faux. Les requĂȘtes serveurâĂ âserveur passent.
- RĂ©ponses dâerreur sans CORS : tu dĂ©bogues Ă lâaveugle (CORS error masque le vrai code).
Le cas âcookies cross-siteâ (le plus risquĂ©)
Si ton API utilise session cookies, un navigateur peut envoyer ces cookies automatiquement sur des requĂȘtes cross-site selon la politique SameSite/secure. CORS intervient seulement pour la lecture de la rĂ©ponse par le script.
Checklist âsafe-ishâ si cookies nĂ©cessaires
- Origines explicitement listées (pas de wildcard)
CORS_ALLOW_CREDENTIALS=TrueCSRF_TRUSTED_ORIGINSconfiguré (si POST authentifiés via cookie)- Cookies marqués
Secureen HTTPS - SameSite ajusté selon architecture
Rappel : âCORS errorâ â sĂ©curitĂ© compromise
Un âCORS errorâ indique le plus souvent une configuration manquante ou trop restrictive. Ce nâest pas un âhackâ, juste le browser qui applique SOP/CORS.
Pattern recommandé : JWT via Authorization header
Câest souvent le plus simple (et le plus clean) pour une SPA sĂ©parĂ©e : le browser envoie Authorization: Bearer ... donc tu dois autoriser ce header cĂŽtĂ© CORS.
# settings.py
CORS_ALLOWED_ORIGINS = ["https://app.example.com"]
CORS_ALLOW_HEADERS = [
"accept","authorization","content-type","origin","user-agent",
"x-requested-with","x-request-id",
]
# frontend (fetch)
fetch("https://api.example.com/v1/private", {
headers: { "Authorization": "Bearer " + token },
})authorization dans CORS_ALLOW_HEADERS, tu auras un prĂ©flight qui Ă©choue.Pattern cookies : utile, mais plus âtouchyâ
# settings.py
CORS_ALLOWED_ORIGINS = ["https://app.example.com"]
CORS_ALLOW_CREDENTIALS = True
# frontend
axios.get("https://api.example.com/v1/me", { withCredentials: true })En plus, si tu utilises CSRF, configure aussi Django :
# (selon Django) CSRF_TRUSTED_ORIGINS = ["https://app.example.com"]
DRF : points pratiques
- Les vues DRF nâont pas besoin de âgĂ©rer CORSâ : câest au middleware.
- Si tu as un endpoint qui renvoie des fichiers (CSV/PDF), expose
Content-Disposition. - Ajoute un
X-Request-ID(corr_id) en reverse proxy/app, puisCORS_EXPOSE_HEADERS.
# Exposer Content-Disposition (downloads) CORS_EXPOSE_HEADERS = ["content-disposition", "x-request-id"]
Recommandation générale
Mets CORS dans Django via django-cors-headers : la logique est proche de lâapp, versionnĂ©e, testable, et cohĂ©rente avec les routes.
- plusieurs apps derriĂšre un gateway unique
- tu veux normaliser / centraliser les headers
- tu gÚres des préflights au niveau edge/CDN
Antiâpatterns courants
- Doubles headers : Django ajoute, Nginx ajoute â incohĂ©rences.
- Nginx rĂ©pond au OPTIONS mais pas Django â ok en prod, KO en dev/staging.
- Cache/CDN qui sert une réponse avec mauvais
Access-Control-Allow-Origin.
Snippet Nginx (si tu centralises)
Exemple âminimalâ (Ă adapter ; prĂ©fĂšre une whitelist stricte).
# location /api/ {
# if ($request_method = OPTIONS) {
# add_header Access-Control-Allow-Origin "https://app.example.com" always;
# add_header Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS" always;
# add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-Request-ID" always;
# add_header Access-Control-Max-Age 86400 always;
# return 204;
# }
# add_header Access-Control-Allow-Origin "https://app.example.com" always;
# add_header Access-Control-Allow-Credentials "true" always;
# proxy_pass http://upstream_django;
# }curl : la mĂ©thode âchirurgicaleâ
# 1) simple request curl -i https://api.example.com/v1/ping \ -H "Origin: https://app.example.com" # 2) preflight curl -i -X OPTIONS https://api.example.com/v1/items \ -H "Origin: https://app.example.com" \ -H "Access-Control-Request-Method: POST" \ -H "Access-Control-Request-Headers: content-type, authorization" # 3) vérifier que l'origine refusée ne reçoit pas d'Allow-Origin curl -i https://api.example.com/v1/ping \ -H "Origin: https://evil.example"
DevTools (Chrome/Firefox)
- Network â filtrer âOPTIONSâ
- Cliquer la requĂȘte OPTIONS â vĂ©rifier Response Headers
- Si 301/302 : vĂ©rifier redirect (slash, httpâhttps)
- Console : noter lâerreur exacte (souvent âblocked by CORS policyâ)
Tests Django / pytest (exemple minimal)
# tests/test_cors.py
import pytest
@pytest.mark.django_db
def test_cors_allows_known_origin(client, settings):
settings.CORS_ALLOWED_ORIGINS = ["https://app.example.com"]
resp = client.get("/v1/ping", HTTP_ORIGIN="https://app.example.com")
assert resp.status_code in (200, 204)
assert resp.headers.get("Access-Control-Allow-Origin") == "https://app.example.com"
@pytest.mark.django_db
def test_cors_blocks_unknown_origin(client, settings):
settings.CORS_ALLOWED_ORIGINS = ["https://app.example.com"]
resp = client.get("/v1/ping", HTTP_ORIGIN="https://evil.example")
# Souvent: pas de header Allow-Origin => browser bloquera
assert resp.headers.get("Access-Control-Allow-Origin") is NonePattern A : API publique + backoffice privé
- API publique consommĂ©e par plusieurs frontends â liste dâorigines
- Admin interne â origine sĂ©parĂ©e + credentials
CORS_ALLOWED_ORIGINS = [ "https://app.example.com", "https://admin.example.com", "https://partner.example.com", ] CORS_ALLOW_CREDENTIALS = True
Pattern B : multi-environnements (dev/staging/prod)
Bon plan : déclarer les origins par env, via variables.
# settings.py
import os
CORS_ALLOWED_ORIGINS = os.getenv("CORS_ALLOWED_ORIGINS","").split(",")
CORS_ALLOWED_ORIGINS = [o.strip() for o in CORS_ALLOWED_ORIGINS if o.strip()]
# .env
CORS_ALLOWED_ORIGINS=https://app.example.com,https://admin.example.comPattern C : preview deployments
Ton frontend a des URLs temporaires, il faut une regex stricte et âsafeâ.
CORS_ALLOWED_ORIGIN_REGEXES = [ r"^https://pr-\d+\.preview\.example\.com$", ]
Pattern D : downloads (CSV/PDF)
Si tu veux lire le nom de fichier cÎté JS : expose Content-Disposition.
CORS_EXPOSE_HEADERS = ["content-disposition"]
Pattern E : Observabilité
- Ajoute
X-Request-ID(corr_id) partout - Expose-le via CORS pour debug front
- Logue lâOrigin sur erreurs CORS (en staging)
CORS_EXPOSE_HEADERS = ["x-request-id"]
Arbre de décision (simple)
2) Est-ce que lâOPTIONS (prĂ©flight) rĂ©pond 2xx/204 ?
3) Est-ce que les headers CORS existent sur la réponse finale (proxy/CDN inclus) ?
4) Est-ce que tu mélanges cookies/CSRF/credentials ?
Top erreurs & solutions
| Erreur / symptĂŽme | Cause | Fix |
|---|---|---|
| âNo 'Access-Control-Allow-Origin' headerâŠâ | Origin non autorisĂ©e, middleware absent ou trop bas | Ajouter corsheaders + ordonner middleware + whitelist origin |
| Préflight 403 | CSRF / auth bloque OPTIONS, ou view interdit | Middleware CORS haut + vérifier CSRF/permissions OPTIONS |
| PrĂ©flight 301/302 | Redirect (slash, httpâhttps) | Corriger URL + CORS avant CommonMiddleware |
| âRequest header field authorization is not allowedâŠâ | Authorization pas dans allow-headers | Ajouter authorization Ă CORS_ALLOW_HEADERS |
| âCredentials flag is true but Allow-Origin is *â | Wildcard + credentials | Origine explicite (pas *) |
| Ăa marche en local, pas en prod | Nginx/CDN modifie headers, ou origin diffĂ©rente | curl sur URL finale + inspecter Response Headers |
| Deux frontends, un seul marche | Whitelist incomplĂšte | Ajouter origin 2 + reâtester prĂ©flight |
| Erreur seulement sur POST/PUT | Préflight déclenché uniquement sur ces méthodes | Tester OPTIONS et allow-headers/methods |
| Download : filename introuvable en JS | Header non exposé | CORS_EXPOSE_HEADERS=["content-disposition"] |
| Tout est ok sauf Safari (parfois) | Cache/preflight/détails navigateur | Réduire complexité, vérifier max-age, tester sans cache |
| Cookies non envoyĂ©s | Front nâutilise pas credentials ou cookie SameSite | credentials include + config cookies/CSRF |
| RĂ©ponses 404/500 âmasquĂ©esâ | RĂ©ponses dâerreur sans CORS | Remonter CorsMiddleware + corriger chaĂźne proxy |
Commandes âdebug packâ
# Voir juste les headers CORS curl -s -D - https://api.example.com/v1/ping \ -H "Origin: https://app.example.com" -o /dev/null | egrep -i "access-control|vary|origin" # Préflight complet curl -s -D - -X OPTIONS https://api.example.com/v1/items \ -H "Origin: https://app.example.com" \ -H "Access-Control-Request-Method: POST" \ -H "Access-Control-Request-Headers: content-type, authorization" -o /dev/null
Officiel (à privilégier)
- PyPI : django-cors-headers
- GitHub : repo officiel
- README : README.rst
- Changelog (sécurité, breaking changes) : CHANGELOG.rst
RĂ©fĂ©rences âcomprĂ©hensionâ
- Guide Django CORS (2025) : StackHawk
- freeCodeCamp (2025) : How to Enable CORS in Django
Outils pratiques
- DevTools Network (Chrome/Firefox) : inspecter OPTIONS + headers
- curl : reproduire préflight exactement
- Tests Django/pytest : verrouiller la config
Ăchelle de difficultĂ© (1 â 10)
| Niveau | Situation | Complexité |
|---|---|---|
| 2/10 | SPA et API sur 2 origines fixes, tokens JWT | Whitelist + allow Authorization |
| 4/10 | Plusieurs frontends + downloads | Expose headers, tests de nonârĂ©gression |
| 6/10 | Preview deployments (origines dynamiques) | Regex stricte + risque sécurité |
| 7/10 | Cookies cross-site + CSRF | SameSite/CSRF trusted origins + E2E |
| 8/10 | CDN / cache / edge proxy + multi-apps | Headers cohérents + Vary: Origin |
| 9/10 | Multi-tenant (origines par client) | Validation stricte + stockage + audit |
Ce qui rend CORS âgalĂšreâ
- Différences de scheme/port entre dev/staging/prod
- Préflight masqué derriÚre des redirects
- Headers supprimés par proxy/CDN
- Credentials + cookies + CSRF (triple combo)
- Regex trop permissives (sécurité)
Baseline âAPI tokenâ (recommandĂ©)
INSTALLED_APPS += ["corsheaders"] MIDDLEWARE = ["corsheaders.middleware.CorsMiddleware"] + MIDDLEWARE CORS_ALLOWED_ORIGINS = [ "https://app.example.com", ] CORS_ALLOW_HEADERS = [ "accept", "authorization", "content-type", "origin", "user-agent", "x-requested-with", "x-request-id", ] CORS_EXPOSE_HEADERS = ["x-request-id"] CORS_PREFLIGHT_MAX_AGE = 86400
Baseline âcookiesâ
CORS_ALLOWED_ORIGINS = ["https://app.example.com"] CORS_ALLOW_CREDENTIALS = True CORS_EXPOSE_HEADERS = ["x-request-id", "content-disposition"] # + CSRF_TRUSTED_ORIGINS cÎté Django si besoin
curl pack
# Simple curl -i https://api.example.com/v1/ping -H "Origin: https://app.example.com" # Preflight (POST + Authorization) curl -i -X OPTIONS https://api.example.com/v1/items \ -H "Origin: https://app.example.com" \ -H "Access-Control-Request-Method: POST" \ -H "Access-Control-Request-Headers: content-type, authorization, x-request-id" # Grep headers CORS curl -s -D - https://api.example.com/v1/ping \ -H "Origin: https://app.example.com" -o /dev/null | egrep -i "access-control|vary|origin"
Checklist DEV
- Whitelist exacte : scheme + port (ex:
http://localhost:5173) authorizationautorisé si JWT- Préflight OPTIONS visible dans Network
- Middleware CORS avant CommonMiddleware
Checklist PROD
- Pas de
ALLOW_ALL - Origines explicitement listées (ou regex trÚs stricte)
- Si credentials : origine explicite + tests E2E
- curl sur URL finale (proxy/CDN inclus)
- Expose
X-Request-IDpour debug - Changelog lu lors des upgrades
fetch
// JWT
fetch("https://api.example.com/v1/private", {
headers: { "Authorization": "Bearer " + token }
});
// Cookies
fetch("https://api.example.com/v1/me", {
credentials: "include"
});axios
// JWT
axios.get("https://api.example.com/v1/private", {
headers: { Authorization: `Bearer ${token}` }
});
// Cookies
axios.get("https://api.example.com/v1/me", { withCredentials: true });