CORS : pourquoi le navigateur bloque une API qui fonctionne pourtant
Comprendre pourquoi le navigateur refuse une réponse d’API venue d’une autre origine, poser les en-têtes attendus, et distinguer ce que CORS protège de ce qu’il ne protège pas.
Où en êtes-vous avec cette notion ?
Une indication personnelle, enregistrée uniquement dans ce navigateur.
À lire ensuite
Le format café
L’essentiel à comprendre, le temps d’un café.
Envie de creuser ? Un cours plus complet vous attend juste après, à déplier sans quitter cette page.
Ce que vous allez comprendre
Pourquoi une API répond parfaitement à curl et échoue depuis le navigateur, ce que CORS autorise réellement, et pourquoi il ne remplace aucune sécurité serveur.
Une origine, c’est trois choses
origine = protocole + domaine + port
https://mon-site.fr et https://api.mon-site.fr sont deux origines différentes. http://localhost:3000 et http://localhost:8000 aussi : le port suffit à les séparer. C’est le cas de développement le plus fréquent.
Ce que CORS décide vraiment
Quand votre JavaScript appelle une autre origine :
fetch('https://api.exemple.test/users');
le navigateur inspecte la réponse et cherche un en-tête :
Access-Control-Allow-Origin: https://mon-site.fr
Sans lui, le navigateur refuse de remettre la réponse à votre code, et la console affiche un message du type « blocked by CORS policy ».
Le même appel depuis curl, depuis Postman ou depuis un serveur Node.js fonctionne : ces clients n’appliquent pas la politique d’origine du navigateur. L’API n’est donc pas cassée.
Le préflight
Dès qu’une requête sort du cadre le plus simple — en-tête Authorization, Content-Type: application/json, méthode DELETE — le navigateur demande d’abord la permission :
OPTIONS /api/users
Le serveur répond avec les origines, méthodes et en-têtes acceptés. Si ce préflight est refusé, la vraie requête n’est jamais envoyée.
CORS n’est pas un pare-feu
C’est le point le plus mal compris. CORS indique au navigateur quelles origines JavaScript peuvent lire une réponse. Une requête dite simple, elle, part sans vérification préalable : le serveur la traite, et seul l’accès à la réponse est ensuite refusé. Un formulaire HTML envoyé vers votre domaine depuis un autre site n’est même pas concerné par CORS.
Ce risque-là porte un autre nom : CSRF, l’envoi d’une requête authentifiée déclenchée par un site tiers. CORS ne s’y oppose pas. Les protections réelles sont un jeton anti-CSRF vérifié côté serveur et l’attribut SameSite sur le cookie de session.
Authentification, autorisation, validation des entrées et limitation de débit restent entièrement côté serveur, indépendamment de CORS.
Le cas des cookies
fetch('https://api.exemple.test/me', { credentials: 'include' });
Le serveur doit alors répondre Access-Control-Allow-Credentials: true et nommer explicitement l’origine autorisée. Le joker * est refusé dans cette configuration, et déconseillé dès qu’une API n’est pas publique.
Fiche mémo
- Origine = protocole + domaine + port.
- Erreur CORS : le navigateur bloque la lecture, pas forcément l’envoi.
- Préflight
OPTIONSavant les requêtes non simples. *incompatible aveccredentials: "include".- CORS n’est ni une protection CSRF ni une authentification.
L’idée à emporter : CORS est une permission de lecture accordée au JavaScript d’une page, pas une serrure posée sur votre API.
Et si on allait plus loin ?
Le café vous a donné les repères. Prenez maintenant le temps de comprendre les mécanismes et de pratiquer, si vous le souhaitez.
Aller plus loinDiagnostiquer une erreur CORS et savoir ce qu’elle ne protège pasDévelopper le coursReplier le cours
Ce que vous saurez faire
Lire une erreur CORS et identifier l’en-tête manquant ; configurer une API pour une application front hébergée ailleurs ; expliquer pourquoi CORS ne remplace ni l’authentification ni une protection CSRF.
1. Reproduire l’erreur en une minute
Contexte d’exécution : la console de développement d’un navigateur, sur n’importe quelle page. Aucun serveur à installer.
// À coller dans la console d’une page quelconque.
fetch('https://api.exemple.test/users')
.then((r) => r.json())
.then(console.log)
.catch((e) => console.error('Bloqué :', e));
Résultat attendu si l’API ne renvoie pas d’en-tête d’autorisation : la promesse est rejetée avec une erreur réseau générique, et le navigateur affiche à côté un message mentionnant la politique CORS. Le domaine exemple.test est illustratif ; remplacez-le par une API que vous contrôlez.
Un détail compte : votre code JavaScript ne reçoit aucune information précise. Le message détaillé n’existe que dans la console. C’est volontaire — donner le motif exact au script reviendrait déjà à lui livrer un renseignement sur la réponse.
Référence : documentation HTTP.
2. Ce que le serveur doit répondre
Contexte d’exécution : une API Node.js que vous administrez. Ce fragment s’insère dans un gestionnaire de requête, il ne s’exécute pas seul.
// Serveur : en-têtes à ajouter à la réponse.
res.setHeader('Access-Control-Allow-Origin', 'https://app.monsite.fr');
res.setHeader('Vary', 'Origin');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
Vary: Origin est souvent oublié. Sans lui, un cache intermédiaire peut servir à une origine la réponse autorisée pour une autre.
Le navigateur envoie d’abord un préflight dès que la requête sort du cadre simple :
Navigateur Serveur
│ OPTIONS /api/users │
│ Origin, Access-Control-… │
├──────────────────────────────►│
│ │
│ 204 + en-têtes autorisés │
│◄──────────────────────────────┤
│ │
│ DELETE /api/users/42 │
├──────────────────────────────►│
Si la réponse au OPTIONS ne convient pas, le DELETE n’est jamais envoyé. Une erreur CORS sur une méthode d’écriture vient donc très souvent d’une route OPTIONS que le serveur ne gère pas.
3. Cookies, jokers, et la frontière avec CSRF
Avec credentials: "include", le navigateur joint les cookies de l’API. Le serveur doit alors renvoyer l’origine exacte et Access-Control-Allow-Credentials: true. La valeur * est refusée dans cette configuration : autoriser toute page du web à lire une réponse authentifiée n’aurait pas de sens.
Deux questions distinctes se cachent ici derrière un même vocabulaire de sécurité.
| Question posée | Mécanisme concerné |
|---|---|
| Quel JavaScript peut lire ma réponse ? | CORS |
| Un site tiers peut-il déclencher une action en mon nom ? | Protection CSRF |
Un formulaire HTML posté vers votre domaine depuis un site malveillant part avec les cookies de la victime et n’est pas soumis à CORS. La réponse restera inaccessible à l’attaquant, mais l’action, elle, aura eu lieu. Les protections réelles sont ailleurs : un jeton anti-CSRF vérifié côté serveur, et un cookie de session en SameSite=Lax ou SameSite=Strict.
Référence : spécification Fetch.
Pièges et limites
- Croire que CORS sécurise l’API. Il filtre la lecture des réponses par le JavaScript d’une page. Authentification, autorisation, validation des entrées et limitation de débit restent votre travail, côté serveur.
- Mettre
*pour faire disparaître l’erreur. Cela ouvre la lecture à toute page web. Sur une API publique en lecture seule c’est acceptable ; sur une API authentifiée c’est une régression, et cela cesse même de fonctionner avec les cookies. - Oublier la route
OPTIONS. Beaucoup de serveurs ne répondent qu’aux méthodes déclarées. Le préflight échoue alors sur un404ou un405, et le navigateur parle de CORS sans nommer la vraie cause. - Confondre CORS et panne réseau. Une API injoignable, un certificat invalide ou une erreur
500produisent aussi une promesse rejetée. Lisez l’onglet réseau avant de conclure. - Valider avec
curlseul.curlignore la notion d’origine : un succès ne prouve rien sur le comportement du navigateur.
À vous de jouer
Votre front tourne sur http://localhost:3000 et appelle votre API sur http://localhost:8000. Le GET /api/produits fonctionne. Le DELETE /api/produits/7, envoyé avec un en-tête Authorization, échoue sur une erreur CORS. Le serveur renvoie pourtant bien Access-Control-Allow-Origin: http://localhost:3000 sur toutes ses réponses.
Que manque-t-il, et comment le vérifier sans ouvrir le navigateur ?
Correction commentée
Le GET sans en-tête particulier est une requête simple : aucun préflight, l’en-tête d’origine suffit. Le DELETE accompagné d’un Authorization ne l’est pas. Le navigateur envoie d’abord OPTIONS /api/produits/7, et c’est cette réponse-là qui est incomplète.
Access-Control-Allow-Methods: GET, POST, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 600
Les deux premiers en-têtes manquent. Le troisième est facultatif : il autorise le navigateur à réutiliser l’autorisation pendant un temps donné, au lieu de redemander avant chaque requête.
Vérification depuis un terminal, sans navigateur :
curl -i -X OPTIONS http://localhost:8000/api/produits/7 \
-H "Origin: http://localhost:3000" \
-H "Access-Control-Request-Method: DELETE" \
-H "Access-Control-Request-Headers: authorization"
Résultat attendu une fois le serveur corrigé : un statut 204 ou 200, accompagné des en-têtes ci-dessus. Si vous obtenez 404 ou 405, la route OPTIONS n’est tout simplement pas gérée : c’est la cause la plus fréquente.
Cette commande reproduit exactement la question que pose le navigateur, ce qu’un simple curl -X DELETE ne fait pas — d’où l’impression trompeuse que « l’API marche ».
Vous pouvez aussi vous arrêter ici. L’approfondissement est facultatif.
Les repères Cours Café
Pour aller à la source
Documentation de référence. Vérification éditoriale encore à effectuer.
Gardez une trace de cette idée.
Favoris, notes et progression seront disponibles après connexion du stockage distant.