CORS commence par la same-origin policy
La same-origin policy est un mécanisme de sécurité fondamental du navigateur. Elle limite la capacité d'un document ou d'un script à lire les données d'un autre origin. Elle empêche notamment un site malveillant de lire les données d'un service où l'utilisateur est déjà connecté. [3]
CORS, Cross-Origin Resource Sharing, est un mécanisme HTTP qui permet au serveur d'assouplir cette règle et de déclarer explicitement quels origins peuvent lire une réponse. Le Fetch Standard le définit comme un protocole opt-in. [1][2]
Ce qu'est exactement un origin
| Page | URL cible | Relation | Pourquoi |
|---|---|---|---|
| https://app.example.com | https://app.example.com/api | Même origin | Même schéma, hôte et port |
| https://app.example.com | https://api.example.com | Origin différent | Hôte différent |
| https://app.example.com | http://app.example.com | Origin différent | Schéma différent |
| https://app.example.com | https://app.example.com:8443 | Origin différent | Port différent |
| https://app.example.com | https://app.example.com/v2 | Même origin | Seul le chemin change |
Un origin est défini par le schéma, l'hôte et le port. Deux URL sont same-origin uniquement si les trois correspondent. Le chemin ne compte pas. [3][4]
`https://app.example.com` et `https://api.example.com` sont donc des origins différents. HTTP et HTTPS diffèrent aussi, tout comme deux ports distincts. [4]
Same-origin et same-site ne sont pas équivalents. C'est important pour les cookies, car `SameSite` utilise la notion de site alors que CORS utilise l'origin. [13]
Ce que signifie réellement navigateur bloque l'API
Dire que le navigateur bloque la requête est souvent trop simplificateur. Pour une requête cross-origin simple, il peut l'envoyer, recevoir une réponse HTTP valide puis refuser de l'exposer à JavaScript si le contrôle CORS échoue. [1][6]
Pour les requêtes avec preflight, le comportement est différent. Le navigateur envoie d'abord `OPTIONS`. Si le preflight échoue, la vraie requête n'est pas envoyée. [2][5][6]
Cette distinction est importante pour les opérations qui modifient des données. Une erreur CORS ne signifie pas automatiquement que le backend n'a reçu aucune requête.
Requêtes simples : quand le preflight est évité
| Méthode | Headers d'exemple | Requête simple | Effet |
|---|---|---|---|
| GET | Accept | Oui | Pas de preflight |
| POST | Content-Type: text/plain | Oui | Pas de preflight |
| POST | Content-Type: application/json | Non | JSON déclenche un preflight |
| PUT | Content-Type: application/json | Non | La méthode déclenche un preflight |
| GET | Authorization: Bearer ... | Non | Le header déclenche un preflight |
Une requête n'est simple que si elle respecte la safelist CORS. Les méthodes autorisées sont `GET`, `HEAD` et `POST`. Les headers ajoutés manuellement doivent être safelisted, et `Content-Type` ne peut utiliser que `application/x-www-form-urlencoded`, `multipart/form-data` ou `text/plain`. [1]
Un `POST` avec `Content-Type: application/json` n'est généralement pas simple et déclenche un preflight. Même chose pour `PUT`, `DELETE` ou un header personnalisé comme `X-Request-ID`. [1][11]
Preflight OPTIONS : ce que vérifie le navigateur
Le preflight est une requête `OPTIONS` automatique. Le navigateur envoie `Origin`, `Access-Control-Request-Method` et si nécessaire `Access-Control-Request-Headers`. Le serveur répond avec les origins, méthodes et headers autorisés. [2][5]
Pour envoyer `DELETE` avec `Authorization`, le navigateur peut d'abord demander si cet origin a le droit d'utiliser cette méthode et ce header. Une réponse positive est nécessaire avant la vraie requête. [5]
Le preflight ajoute un aller-retour réseau, mais son résultat peut être stocké dans un cache CORS dédié, séparé du cache HTTP normal. [5][10]
Les principaux headers CORS
| Header | Direction | Rôle |
|---|---|---|
| Origin | Requête | Origin qui initie la requête |
| Access-Control-Allow-Origin | Réponse | Origin dont le code peut recevoir la réponse |
| Access-Control-Allow-Methods | Réponse preflight | Méthodes autorisées par le preflight |
| Access-Control-Allow-Headers | Réponse preflight | Headers de requête autorisés par le preflight |
| Access-Control-Allow-Credentials | Réponse | Autorisation d'exposer une réponse avec credentials |
| Access-Control-Expose-Headers | Réponse | Headers de réponse supplémentaires visibles par JavaScript |
| Access-Control-Max-Age | Réponse preflight | Durée de cache du résultat du preflight |
| Vary: Origin | Réponse | Indique au cache que la réponse varie selon Origin |
`Origin` est le header de requête qui identifie l'origin initiateur. `Access-Control-Allow-Origin` est un header de réponse indiquant au navigateur si la réponse peut être partagée avec le code de cet origin. [1][2][8]
`Access-Control-Allow-Methods` et `Access-Control-Allow-Headers` sont particulièrement importants pour le preflight. `Access-Control-Allow-Credentials` autorise l'exposition d'une réponse avec credentials et `Access-Control-Expose-Headers` peut exposer des headers supplémentaires à JavaScript. [1][2]
Quand le serveur renvoie dynamiquement `Access-Control-Allow-Origin`, MDN recommande également `Vary: Origin`. [1]
`Access-Control-Allow-Origin` : wildcard ou origin explicite
`Access-Control-Allow-Origin: *` permet de partager une réponse avec tout origin pour les requêtes sans credentials. Cela peut être correct pour une API réellement publique. [2][8]
Pour des endpoints privés ou sensibles, l'ensemble autorisé doit rester minimal. MDN et OWASP recommandent des origins spécifiques lorsque l'accès global n'est pas nécessaire. [14][17]
Avec plusieurs origins autorisés, le serveur doit comparer l'`Origin` reçu à une allowlist et ne renvoyer que la valeur validée. Il ne faut pas refléter aveuglément n'importe quel origin. [8][16]
Cookies et credentials : source fréquente d'erreurs
Fetch peut utiliser `credentials: "include"` pour demander l'envoi de credentials sur une requête cross-origin. Cela ne suffit pas côté client. Le serveur doit renvoyer `Access-Control-Allow-Credentials: true` et un `Access-Control-Allow-Origin` explicite. `*` est interdit dans ce cas. [2][6][9]
Les cookies suivent aussi leurs propres règles. `SameSite` peut empêcher l'envoi d'un cookie sur une requête cross-site, et `SameSite=None` nécessite `Secure`. [13]
CORS et `SameSite` résolvent des problèmes différents. CORS peut être correct alors que le cookie n'est pas envoyé, ou le cookie peut être envoyé alors que la réponse reste inaccessible à JavaScript.
Pourquoi `mode: "no-cors"` ne corrige généralement rien
`fetch(..., { mode: "no-cors" })` ne contourne pas CORS lorsqu'une application doit lire la réponse. Les méthodes et headers sont limités, et la réponse devient opaque. JavaScript ne peut lire ni body ni headers, et le statut exposé vaut `0`. [6][7][11]
`no-cors` a des usages spécialisés, notamment certains cas Service Worker, mais c'est généralement une mauvaise réponse pour une API JSON classique. [6]
Si l'API externe ne propose pas CORS, un backend ou proxy que vous contrôlez peut effectuer la requête serveur à serveur. [11][12]
Pourquoi curl, Postman ou un backend peuvent fonctionner
La same-origin policy et CORS sont appliqués par le navigateur. Un client HTTP hors de ce modèle n'est pas soumis au même contrôle. C'est pourquoi un endpoint peut fonctionner avec curl ou un outil API tout en étant bloqué depuis le JavaScript d'une page. [2][3][16]
Cela montre aussi que CORS n'est pas une authentification API. Un client hors navigateur peut choisir son propre `Origin`, donc OWASP déconseille d'utiliser ce header comme preuve d'identité. [16]
L'API a toujours besoin d'autorisation, tokens, sessions, contrôles de droits et validation côté serveur.
CORS ne remplace ni CSRF ni l'autorisation
La same-origin policy limite surtout la lecture cross-origin. Les formulaires et certaines requêtes simples peuvent néanmoins être envoyés depuis un autre site. Une application basée sur les cookies a donc toujours besoin de protections CSRF. [3][15]
OWASP recommande explicitement de ne pas utiliser CORS ou `Origin` seuls comme contrôle d'accès à des ressources sensibles. Authentification et autorisation restent nécessaires. [16]
CORS répond à la question de savoir si le navigateur peut exposer une réponse au code d'un origin. Il ne dit pas si l'utilisateur ou le client est autorisé à effectuer l'opération.
Erreurs fréquentes et diagnostic
L'absence de `Access-Control-Allow-Origin` est une erreur courante. D'autres causes sont une méthode interdite, un header manquant dans `Access-Control-Allow-Headers`, l'utilisation de `*` avec credentials, une mauvaise réponse OPTIONS ou des redirects dans le flux CORS. [11][12]
JavaScript reçoit volontairement peu de détails. La console et le panneau Network du navigateur donnent les informations utiles. [1][11]
`CORS request did not succeed` peut aussi provenir de DNS, timeout, refus de connexion, TLS, mixed content ou extensions. [11]
- Vérifier l'origin exact du frontend : schéma, hôte et port.
- Inspecter `OPTIONS` s'il existe.
- Contrôler le statut et tous les `Access-Control-Allow-*` du preflight.
- Vérifier aussi les headers CORS de la réponse réelle.
- Avec cookies, vérifier `credentials`, `Access-Control-Allow-Credentials`, `SameSite` et `Secure`.
- Contrôler redirects, TLS, mixed content et erreurs réseau.
- Comparer la requête navigateur à une requête curl ou outil API qui fonctionne.
Configurations CORS dangereuses
Refléter sans validation l'`Origin` reçu est dangereux lorsque des requêtes avec credentials renvoient des données sensibles. Un attaquant peut contrôler un origin et tenter de lire des réponses dans le contexte d'un utilisateur connecté. [16]
Des expressions régulières trop larges et la confiance dans tous les sous-domaines sont aussi risquées si l'un d'eux peut être compromis. OWASP recommande une allowlist avec correspondance précise. [16][17]
`Access-Control-Allow-Origin: *` n'est pas automatiquement une faille pour des données entièrement publiques sans credentials. Le problème est une politique plus permissive que nécessaire. [8][17]
Performance : coût du preflight et `Access-Control-Max-Age`
Le preflight ajoute un `OPTIONS` avant la vraie requête et peut donc augmenter la latence. Son résultat peut être mis en cache avec `Access-Control-Max-Age` dans un cache séparé du cache HTTP normal. [5][10]
MDN indique une valeur par défaut de 5 secondes en l'absence du header. Les navigateurs imposent leurs propres maximums. Firefox limite à 86400 secondes et Chromium depuis la version 76 à 7200 secondes. [10]
Il est utile de mettre en cache une politique stable, mais pas au point de rendre difficile le retrait rapide d'une règle trop permissive.
Configuration pratique et checklist de déploiement
Commencez par décider si l'endpoint doit réellement être cross-origin. Sinon, n'ajoutez pas de headers CORS. Si oui, définissez les origins, méthodes et headers nécessaires ainsi que le besoin éventuel de credentials. [14][17]
Pour une allowlist dynamique, ne renvoyez qu'un origin validé et ajoutez `Vary: Origin`. Avec credentials, utilisez un origin explicite et `Access-Control-Allow-Credentials: true`. Le frontend ne peut pas remplacer cette décision serveur. [1][2]
Après déploiement, testez une requête simple, un preflight, un scénario cookie, un origin refusé et les erreurs réelles dans DevTools.
- L'accès cross-origin est-il réellement nécessaire ?
- Quels origins précis sont autorisés ?
- Les credentials sont-ils nécessaires ?
- Quelles méthodes et quels headers de requête sont nécessaires ?
- `OPTIONS` atteint-il l'application et répond-il correctement ?
- La réponse réelle contient-elle aussi les bons headers CORS ?
- Les réponses dynamiques incluent-elles `Vary: Origin` ?
- L'endpoint exige-t-il toujours authentification et autorisation normales ?
- La politique a-t-elle été testée avec un origin autorisé et un origin refusé ?

