Nous avons tous déjà vécu cette situation : vous envoyez une requête API, vous attendez la réponse, et boum, vous êtes confronté à une «erreur CORS» qui s'affiche dans la console de votre navigateur.
Pour de nombreux développeurs, la première réaction est de trouver une solution rapide : ajouter Access-Control-Allow-Origin: * et passer à autre chose. Cependant, cette approche passe complètement à côté de l'essentiel. CORS n'est pas simplement un obstacle de configuration supplémentaire, mais l'un des mécanismes de sécurité les plus importants jamais créés pour les navigateurs.
Le CORS, ou Cross-Origin Resource Sharing (partage de ressources entre origines), existe pour protéger les utilisateurs tout en permettant une communication interdomaines légitime entre les applications web. Pourtant, il est souvent mal compris, mal configuré ou considéré comme un bug à « contourner ».
Mais plus maintenant.
Dans ce guide, nous irons au-delà des notions de base. Vous apprendrez :
- Pourquoi CORS existe-t-il et comment a-t-il évolué à partir de la politique de même origine (SOP) ?
- Comment les navigateurs et les serveurs négocient réellement l'accès inter-origines
- Qu'est-ce qui fait échouer certaines configurations CORS, même lorsqu'elles « semblent correctes » ?
- Comment gérer en toute sécurité les requêtes pré-vol, les informations d'identification et les particularités des navigateurs
À la fin, vous saurez non seulement comment configurer CORS, mais vous comprendrez également pourquoi il se comporte ainsi et comment concevoir vos API en toute sécurité.
Qu'est-ce que CORS (et pourquoi existe-t-il) ?
CORS est une norme de sécurité pour navigateurs qui définit comment les applications web d'une origine donnée peuvent accéder en toute sécurité aux ressources d'une autre origine.
Pour comprendre la sécurité CORS, vous devez d'abord savoir pourquoi elle a été créée.
Bien avant que les API et les microservices ne prennent le contrôle du Web, les navigateurs suivaient une règle simple appelée « Same-Origin Policy » (SOP).
Cette politique stipulait qu'une page Web ne pouvait envoyer et recevoir des données qu'à partir de la même origine, c'est-à-dire le même protocole, le même domaine et le même port.
Par exemple :
Cette restriction était tout à fait logique au début du Web, lorsque la plupart des sites Web étaient monolithiques. Un seul site hébergeait son front-end, son back-end et ses ressources sous un seul domaine.
Mais à mesure que le Web évoluait avec les API, les microservices et les intégrations tierces, cette même règle est devenue un obstacle. Les développeurs avaient besoin d'applications frontales pour communiquer avec d'autres domaines, tels que :
- www.example.com communique avec api.example.com
- Votre application se connectant à un CDN ou à un point de terminaison d'analyse
- Clients Web appelant des API tierces (comme Stripe ou Google Maps)
La politique de même origine est devenue un obstacle qui bloquait les architectures modernes et distribuées.
C'est là qu'est intervenu le partage de ressources entre origines (CORS).
Plutôt que de supprimer complètement les restrictions des navigateurs, CORS a introduit un assouplissement contrôlé du SOP. Il a créé un moyen sécurisé pour les navigateurs et les serveurs de communiquer entre les domaines, en toute sécurité, et uniquement lorsque les deux parties sont d'accord.
Imaginez cela ainsi : SOP est une porte verrouillée qui ne laisse entrer personne, tandis que CORS est la même porte, mais avec une liste d'invités et un videur qui vérifie les pièces d'identité.
Cet équilibre entre flexibilité et protection rend la configuration CORS essentielle pour toutes les applications web modernes.
Comprendre la politique de même origine (SOP)
Avant d'aller plus loin dans la configuration CORS, il est essentiel de comprendre son principe fondamental : la politique de même origine (SOP).
Comme indiqué précédemment, le SOP est la première ligne de défense du navigateur contre les comportements malveillants sur le Web. Il empêche un site Web d'accéder librement aux données d'un autre site, ce qui pourrait exposer des informations sensibles telles que les cookies, les jetons d'authentification ou les données personnelles.
Voici comment cela fonctionne dans la pratique : lorsqu'une page Web se charge dans votre navigateur, une origine lui est attribuée en fonction de trois éléments : le protocole, l'hôte et le port :
https:// api.example.com :443
^ ^ ^
protocole hôte port
Deux URL sont considérées comme provenant de la même origine uniquement si ces trois parties correspondent. Sinon, le navigateur les traite comme provenant d'origines différentes.
Cette règle simple empêche les actions intersites nuisibles. Sans elle, un site aléatoire pourrait charger votre tableau de bord bancaire en ligne dans un cadre invisible, lire votre solde et l'envoyer à un pirate, le tout sans votre consentement.
En bref, le SOP existe pour isoler le contenu entre différents sites, garantissant ainsi que chaque origine constitue une zone de sécurité autonome.
Pourquoi la SOP seule ne suffisait pas
La politique de même origine fonctionnait parfaitement lorsque les sites Web étaient autonomes. Mais à mesure que le Web a évolué vers un écosystème d'API, de microservices et d'architectures distribuées, cette règle stricte est devenue une limitation majeure.
Les applications modernes doivent :
- Appeler leurs propres API hébergées sur différents sous-domaines (app.example.com → api.example.com)
- Récupérer des ressources à partir de CDN ou de services tiers
- Intégrez des API externes telles que Stripe, Firebase ou Google Maps.
Dans le cadre du SOP, ces requêtes inter-origines légitimes ont été bloquées. Les développeurs ont essayé toutes les solutions possibles, y compris JSONP, les proxys inversés ou les domaines dupliqués, mais ces correctifs étaient soit peu sûrs, soit extrêmement complexes.
C'est là que CORS (Cross-Origin Resource Sharing) a changé la donne.
CORS a introduit un système de poignée de main qui permettait aux navigateurs et aux serveurs de négocier la confiance. Au lieu de rompre le SOP, il l'a étendu, offrant un moyen de mettre en liste blanche de manière sécurisée des origines spécifiques pour la communication interdomaines.
Fonctionnement du CORS : le flux au niveau du protocole
Comme indiqué précédemment, lorsque votre navigateur envoie une requête à une autre origine, il ne la transmet pas aveuglément. Il suit plutôt un protocole CORS bien défini : un échange entre le navigateur et le serveur afin de déterminer si la requête doit être autorisée.
À la base, CORS fonctionne grâce aux en-têtes HTTP. Le navigateur ajoute un en-tête Origin à chaque requête inter-origines, indiquant au serveur d'où provient la requête. Le serveur répond alors avec un ou plusieurs en-têtes Access-Control-* qui définissent ce qui est autorisé.
Voici un exemple simplifié de cette conversation :
# Request
GET /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
# Response
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Content-Type: application/json
{"message": "Success"}
Dans ce cas, le serveur autorise explicitement l'origine https://app.example.com à accéder à sa ressource. Le navigateur vérifie cette réponse, confirme la correspondance et transmet les données à votre JavaScript.
Mais si les origines ne correspondent pas ou si les en-têtes de réponse sont manquants ou incorrects, le navigateur bloque silencieusement la réponse. Vous ne verrez pas les données, seulement ce message frustrant « Erreur CORS » dans votre console.
Il est important de noter que CORS ne rend pas un serveur plus sécurisé en soi. Il applique plutôt des règles d'engagement entre les navigateurs et les serveurs, une couche de sécurité qui garantit que seules les origines fiables peuvent accéder aux ressources protégées.
Types de requêtes CORS
CORS définit deux types principaux de requêtes : simples et prévalidées. La différence réside dans le niveau de vérification effectué par le navigateur avant d'envoyer les données.
1. Demandes simples
Une requête simple est le type le plus direct. Elle est automatiquement autorisée par les navigateurs tant qu'elle respecte certaines règles spécifiques :
- Utilise l'une des méthodes suivantes : GET, HEAD ou POST.
- Inclut uniquement certains en-têtes :
- Accepter
- Accepter la langue
- Langue du contenu
- Type de contenu (mais uniquement application/x-www-form-urlencoded, multipart/form-data ou text/plain)
- N'utilise pas d'en-têtes ou de flux personnalisés
Voici à quoi cela ressemble :
# Request
GET /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
# Response
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Content-Type: application/json
{"message": "This is the response data"}Dans ce cas :
- Le navigateur ajoute automatiquement l'en-tête Origin.
- Le serveur doit renvoyer Access-Control-Allow-Origin avec une origine correspondante.
- Si l'origine ne correspond pas ou est manquante, le navigateur bloque la réponse.
2. Demandes préalables au vol
Les choses deviennent plus intéressantes avec les requêtes non simples. Par exemple, lorsque vous utilisez des méthodes telles que PUT, DELETE ou des en-têtes personnalisés tels que Authorization.
Avant d'envoyer la requête proprement dite, le navigateur effectue une vérification préalable à l'aide d'une requête OPTIONS. Cette étape permet de s'assurer que le serveur autorise explicitement l'opération prévue.
Voici un exemple :
# Preflight Request
OPTIONS /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: content-type, authorization
# Preflight Response
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: PUT, POST, GET, DELETE
Access-Control-Allow-Headers: content-type, authorization
Access-Control-Max-Age: 3600
# Actual Request (only sent if preflight succeeds)
PUT /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Content-Type: application/json
Authorization: Bearer token123
{"data": "update this resource"}Dans cette séquence :
- Le navigateur détecte une requête non simple.
- Il envoie une requête OPTIONS préalable, demandant l'autorisation pour la méthode et les en-têtes réels.
- Le serveur répond en indiquant les méthodes, les en-têtes et les origines qu'il autorise.
- Si la vérification préalable est réussie, le navigateur envoie la requête réelle. Sinon, il la bloque.
Gestion des informations d'identification dans CORS
Lorsqu'il s'agit d'API qui nécessitent une authentification, comme les cookies, les jetons ou les connexions basées sur une session, CORS se comporte différemment.
Par défaut, les navigateurs traitent les requêtes inter-origines comme non authentifiées pour des raisons de sécurité. Cela signifie que les cookies ou les en-têtes d'authentification HTTP ne sont pas inclus automatiquement.
Pour activer les demandes authentifiées en toute sécurité, deux étapes clés doivent être alignées :
1. Le client doit explicitement autoriser les informations d'identification :
fetch('https://api.example.com/data', {
credentials: 'include'
})2. Le serveur doit les autoriser explicitement :
Access-Control-Allow-Credentials : true
Mais il y a un hic, et il est de taille.
Lorsque Access-Control-Allow-Credentials est défini sur true, vous ne pouvez pas utiliser de caractère générique (*) dans Access-Control-Allow-Origin. Les navigateurs rejetteront la réponse si vous essayez.
En effet, autoriser toutes les origines à envoyer des requêtes authentifiées irait à l'encontre de l'objectif même de la sécurité CORS, car cela permettrait à n'importe quel site Internet d'accéder aux données privées liées à la session d'un utilisateur.
Donc, au lieu de cela :
Access-Control-Allow-Origin : *
Access-Control-Allow-Credentials : trueVous devez toujours utiliser une origine spécifique :
Access-Control-Allow-Origin : https://yourapp.com
Access-Control-Allow-Credentials : trueSi votre API dessert plusieurs domaines de confiance, vous pouvez renvoyer dynamiquement l'en-tête d'origine correcte côté serveur :
const allowedOrigins = ['https://app1.com', 'https://app2.com'];
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
}Cette approche garantit que vos demandes authentifiées restent sécurisées et intentionnelles, et ne sont pas accessibles à quiconque tente de les utiliser.
Comment les navigateurs déterminent quelles requêtes sont éligibles au CORS
Avant même qu'une requête n'atteigne votre serveur, le navigateur décide si elle relève des règles CORS.
Cette décision dépend de l'origine de la requête et du fait qu'elle cible un autre domaine, port ou protocole.
Par exemple :
- Demande d'https://api.example.coms à partir d'une page hébergée sur https://example.com : ✅ CORS s'applique (sous-domaine différent).
- Demande d'https://example.com:3000s à partir de https://example.com: ✅ CORS s'applique (port différent).
- Demande d'https://example.coms provenant du même domaine et du même port : ❌ CORS ne s'applique pas.
Si le navigateur détecte qu'une requête traverse plusieurs origines, il inclut automatiquement l'en-tête Origin dans la requête :
Source : https://example.com
Cet en-tête indique au serveur d'où provient la requête et c'est ce que le serveur utilise pour décider d'autoriser ou de bloquer l'accès.
Si la réponse ne comporte pas les en-têtes appropriés (tels que Access-Control-Allow-Origin), le navigateur bloque simplement l'accès à la réponse, même si le serveur en a techniquement envoyé une.
Il s'agit là d'une distinction importante : c'est le navigateur qui applique CORS, et non le serveur.
Contrôles de sécurité internes, XMLHttpRequest vs Fetch et différences entre navigateurs
Tous les navigateurs ne gèrent pas CORS de la même manière, mais ils suivent tous le même modèle de sécurité : ne jamais faire confiance aux données provenant d'autres origines, sauf autorisation explicite.
Ce qui diffère, c'est la rigueur avec laquelle ils appliquent les règles et les API auxquelles ils les appliquent.
1. Le contrôle de sécurité interne CORS
Lorsqu'un navigateur reçoit une réponse à une requête inter-origines, il effectue une étape de validation interne avant d'exposer la réponse à votre code JavaScript.
Il vérifie les en-têtes tels que :
- Access-Control-Allow-Origin : doit correspondre à l'origine de la requête (ou être * dans certains cas).
- Access-Control-Allow-Credentials : doit être vrai si des cookies ou des jetons d'authentification sont impliqués.
- Access-Control-Allow-Methods et Access-Control-Allow-Headers : doivent correspondre à la requête préliminaire d'origine si celle-ci a été envoyée.
Si l'un de ces contrôles échoue, le navigateur ne génère pas d'erreur HTTP, il bloque simplement l'accès à la réponse et enregistre une erreur CORS dans la console.
Cela complique le débogage, car la requête réseau a bien abouti, mais le navigateur masque le résultat par mesure de sécurité.
2. XMLHttpRequest vs Fetch
XMLHttpRequest et l'API fetch() moderne prennent tous deux en charge CORS, mais leur comportement diffère légèrement en matière d'informations d'identification et de paramètres par défaut.
Avec XMLHttpRequest :
- Les cookies et l'authentification HTTP sont envoyés automatiquement si withCredentials est défini sur true.
- Le comportement avant le vol dépend de l'ajout ou non d'en-têtes personnalisés.
Avec Fetch :
- Les informations d'identification (cookies, authentification HTTP) ne sont pas incluses par défaut.
- Vous devez les activer explicitement à l'aide de :
fetch("https://api.example.com/data", {
credentials: "include"
});- fetch traite également les redirections de manière plus stricte dans le cadre du CORS, car il ne suit pas les redirections inter-origines, sauf si elles sont autorisées.
Ainsi, bien que fetch soit plus propre et plus moderne, il est également moins indulgent lorsque vous oubliez un en-tête ou que vous ne respectez pas une règle d'authentification.
3. Différences et particularités entre les navigateurs
Même si la spécification CORS est standard, les navigateurs l'implémentent avec de légères différences :
- Safari peut être trop strict avec les cookies et les demandes d'identification, en particulier lorsque les cookies tiers sont bloqués.
- Firefox met parfois en cache les réponses de prévisualisation ayant échoué plus longtemps que prévu, ce qui entraîne des résultats incohérents lors des tests.
- Chrome applique CORS de manière plus stricte sur certaines chaînes de redirection que sur d'autres.
En raison de ces différences, une configuration qui fonctionne parfaitement sur un navigateur peut échouer silencieusement sur un autre.
C'est pourquoi il est essentiel de tester les configurations CORS sur différents navigateurs, en particulier lorsque des informations d'identification ou des redirections sont impliquées.
Traitement côté serveur de l'en-tête Origin
Bien que le navigateur applique CORS, la décision finale est prise au niveau du serveur.
Lorsque le navigateur envoie une requête inter-origines, il inclut toujours l'en-tête Origin. Le serveur a pour tâche d'inspecter cet en-tête, de décider s'il doit l'autoriser et de renvoyer les en-têtes CORS corrects en réponse.
1. Validation de l'origine
Une demande type pourrait se présenter comme suit : Origine : https://frontend.example.com
Sur le serveur, votre code doit vérifier si cette origine est autorisée. L'approche la plus simple (et la plus sûre) consiste à maintenir une liste blanche des domaines de confiance :
const allowedOrigins = ["https://frontend.example.com", "https://admin.example.com"];
if (allowedOrigins.includes(req.headers.origin)) {
res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
}Cela garantit que seuls les clients connus peuvent accéder à votre API, tandis que les autres ne reçoivent aucune autorisation CORS.
Évitez de renvoyer Access-Control-Allow-Origin: * si votre API gère des cookies, des jetons ou d'autres informations d'identification.
2. Traitement des demandes de prévol
Pour les requêtes OPTIONS pré-vol, le serveur doit répondre avec le même soin que pour les requêtes principales.
Une réponse pré-vol complète comprend :
Access-Control-Allow-Origin : https://frontend.example.com
Access-Control-Allow-Methods : GET, POST, OPTIONS
Access-Control-Allow-Headers : Content-Type, Authorization
Access-Control-Max-Age : 86400Ces en-têtes indiquent au navigateur ce qui est autorisé et combien de temps il peut mettre en cache cette décision. Si l'un d'entre eux est manquant ou incorrect, le navigateur bloquera la requête suivante, même si votre point de terminaison fonctionne correctement.
3. Définition dynamique des en-têtes CORS
Dans les systèmes de grande envergure (tels que les plateformes multi-locataires ou les API avec plusieurs clients), les origines autorisées peuvent devoir être dynamiques.
Par exemple :
const origin = req.headers.origin;
if (origin && origin.endsWith(".trustedclient.com")) {
res.setHeader("Access-Control-Allow-Origin", origin);
}Ce modèle autorise tous les sous-domaines d'un domaine approuvé tout en continuant à filtrer les sources inconnues.
Veillez simplement à valider soigneusement les origines et à ne pas effectuer de comparaison de chaînes sur les entrées utilisateur sans contraintes, sinon les pirates pourraient falsifier des en-têtes qui semblent valides.
4. Pourquoi « ça fonctionne dans Postman » ne signifie pas que la configuration est correcte
L'une des plus grandes idées reçues concernant CORS est la suivante : « Cela fonctionne dans Postman, donc cela doit être un problème lié au navigateur. »
Postman n'applique pas du tout CORS, car ce n'est pas un navigateur.
Cela signifie que même une API entièrement ouverte sans en-têtes Access-Control-* fonctionnera correctement dans ce navigateur, mais échouera immédiatement dans Chrome ou Firefox.
Si votre API fonctionne dans Postman mais pas dans votre application web, vos en-têtes CORS sont probablement incomplets ou mal configurés.
Configurations CORS courantes incorrectes (et comment les éviter)
1. Utilisation de Access-Control-Allow-Origin: * avec des informations d'identification
C'est l'erreur la plus fréquente et la plus dangereuse.
Si votre réponse comprend les deux :
Access-Control-Allow-Origin : *
Access-Control-Allow-Credentials : true…le navigateur bloquera automatiquement la requête.
La spécification CORS interdit l'utilisation de caractères génériques lorsque des informations d'identification sont incluses, car cela permettrait à n'importe quel site d'accéder aux données utilisateur liées aux cookies ou aux jetons d'authentification.
Correction : toujours renvoyer une origine spécifique lorsque des informations d'identification sont utilisées :
Access-Control-Allow-Origin : https://app.example.com
Access-Control-Allow-Credentials : true2. Oublier de traiter les demandes de prévol
De nombreuses API répondent correctement aux requêtes GET et POST, mais oublient la requête préliminaire OPTIONS.
Dans ce cas, le navigateur n'atteint jamais votre point de terminaison réel et bloque la requête principale après l'échec du pré-vol.
Correction : traiter explicitement les requêtes OPTIONS et répondre avec les en-têtes appropriés :
if (req.method === "OPTIONS") {
res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type,Authorization");
return res.sendStatus(204);
}3. En-têtes de requête et de réponse mal alignés
Autre problème subtil : votre requête préliminaire peut demander certains en-têtes, mais le serveur ne les autorise pas explicitement.
Par exemple, si votre demande comprend :
En-têtes de requête de contrôle d'accès : Autorisation, Type de contenu
… mais le serveur répond uniquement par :
Access-Control-Allow-Headers : Content-Type
…le navigateur le bloque. Les deux listes doivent correspondre exactement.
Correction : assurez-vous que votre Access-Control-Allow-Headers inclut tous les en-têtes que le client pourrait envoyer, en particulier Authorization, Accept et les en-têtes personnalisés.
4. Renvoi de plusieurs en-têtes Access-Control-Allow-Origin
Certains proxys ou frameworks mal configurés envoient plusieurs en-têtes Access-Control-Allow-Origin (par exemple, une origine statique * et une origine dynamique).
Les navigateurs considèrent cela comme invalide et bloquent complètement la requête.
Correction : renvoyer systématiquement un seul en-tête Access-Control-Allow-Origin valide.
5. Oublier les restrictions liées à la méthode
Si vous n'incluez pas toutes les méthodes autorisées dans Access-Control-Allow-Methods, les navigateurs rejetteront les requêtes légitimes.
Par exemple, une API peut prendre en charge PUT, mais votre réponse préliminaire n'autorise que GET et POST.
Correction : répertoriez toutes les méthodes prises en charge ou faites correspondre dynamiquement vos routes API pour garantir la cohérence.
6. Ignorer les réponses de prévisualisation mises en cache
Les navigateurs modernes mettent en cache les résultats des prévols pour améliorer les performances.
Mais si votre serveur ou votre CDN met en cache les réponses sans les faire varier en fonction de l'origine, vous pourriez accidentellement envoyer les mauvais en-têtes CORS à un autre client.
Correction : utilisez l'en-tête Vary: Origin pour vous assurer que les réponses sont mises en cache séparément pour chaque origine.
Les problèmes CORS proviennent rarement d'une seule erreur majeure. Ils résultent généralement de plusieurs petits décalages entre les attentes du navigateur et la configuration du serveur. Comprendre ces schémas vous aide à éviter les cycles de débogage sans fin liés aux « erreurs CORS ».
Le CORS n'est pas l'ennemi, c'est son incompréhension qui l'est.
À première vue, CORS semble être un obstacle inutile, ou plutôt un gardien qui bloque vos requêtes et ralentit le développement.
Mais en réalité, c'est l'une des fonctionnalités de sécurité les plus importantes jamais développées pour les navigateurs.
Une fois que vous comprenez comment cela fonctionne, vous cessez de considérer les « erreurs CORS » comme des défaillances aléatoires. Elles deviennent alors des signaux indiquant que votre client et votre serveur doivent mieux s'aligner en matière de confiance, d'en-têtes ou d'informations d'identification.
Que vous développiez une application monopage ou un écosystème API distribué, CORS est votre allié pour assurer la sécurité des utilisateurs tout en permettant une communication interdomaines sécurisée.
La prochaine fois que vous rencontrerez ce message familier sur votre console, ne vous précipitez pas sur le caractère générique. Lisez les en-têtes, suivez la logique et laissez votre compréhension, et non un hack aléatoire, guider la correction !
Sécurisez votre logiciel dès maintenant.



.avif)
