13 - CORS : les requêtes cross-origin demystifiees
Ce que tu vas apprendre
- Ce qu'est la same-origin policy et pourquoi elle existe
- La différence entre requêtes simples et requêtes avec preflight
- Chaque header Access-Control-* et son rôle
- Les restrictions du wildcard *
- Le mode credentials et ses pièges
- Les erreurs CORS courantes et comment les corriger
Prerequisites
Si tu as deja fait du développement front-end, tu as vu cette erreur. Celle qui apparaît en rouge dans la console et qui te fait perdre une heure :
Access to fetch at 'https://api.example.com/data' from origin
'https://app.example.com' has been blocked by CORS policy.
CORS est la source de frustration numero un des développeurs front-end. Et cette frustration vient presque toujours d'un malentendu. CORS n'est pas un mur. C'est une porte avec un vigile. Et le vigile suit des regles precises.
La same-origin policy : le problème que CORS resout
Par défaut, un navigateur interdit a une page de faire des requêtes vers une origine différente. Deux URLs ont la meme origine si elles partagent le meme schema, le meme hote et le meme port :
https://app.example.com:443 -- origine
| | |
schema hote port
https://app.example.com et https://api.example.com sont des origines différentes (hote différent). http://example.com et https://example.com aussi (schema différent). Meme https://example.com:443 et https://example.com:8080 (port différent).
Cette restriction existe pour une bonne raison. Sans elle, un site malveillant pourrait faire des requêtes vers ta banque en ligne en utilisant tes cookies de session. La same-origin policy est un garde-fou fondamental.
Mais elle est trop restrictive pour le web moderne. Une SPA sur app.example.com a besoin d'appeler une API sur api.example.com. C'est la que CORS intervient.
CORS : le serveur autorise les origines
CORS (Cross-Origin Resource Sharing) est un mecanisme ou le serveur déclaré explicitement quelles origines peuvent acceder a ses ressources. C'est le serveur qui décidé, pas le client.
Le navigateur envoie automatiquement l'origine dans la requête :
GET /api/data HTTP/1.1
Origin: https://app.example.com
Le serveur répond avec l'origine autorisee :
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Le navigateur compare les deux. Si ca correspond, il laisse le JavaScript acceder a la réponse. Sinon, il bloque. La requête a bien ete envoyee et le serveur a bien repondu. Mais le navigateur refuse de donner la réponse au JavaScript.
C'est un point subtil. CORS ne bloque pas la requête cote serveur. Le serveur recoit tout. C'est le navigateur qui filtre la réponse. Un outil comme curl ignore complètement CORS.
Requetes simples vs preflight
Toutes les requêtes cross-origin ne sont pas traitees de la meme facon.
Requetes simples
Une requête est "simple" si elle remplit ces conditions :
- Méthode GET, HEAD ou POST
- Headers limites a Accept, Accept-Language, Content-Language, Content-Type
- Content-Type limite a
application/x-www-form-urlencoded,multipart/form-dataoutext/plain
Pour une requête simple, le navigateur envoie directement la requête avec le header Origin. Pas d'étape supplementaire.
Requetes avec preflight
Tout le reste déclenché un preflight : une requête OPTIONS automatique que le navigateur envoie avant la vraie requête.
OPTIONS /api/data HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization
Le navigateur demande : "est-ce que j'ai le droit de faire un PUT avec ces headers depuis cette origine ?"
Le serveur répond :
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
Si la réponse autorise la méthode et les headers demandes, le navigateur envoie la vraie requête. Sinon, il bloque et affiche l'erreur CORS dans la console.
Le Access-Control-Max-Age: 86400 dit au navigateur de cacher cette autorisation pendant 24 heures. Ca évité un preflight a chaque requête.
Les headers Access-Control-* en détail
Access-Control-Allow-Origin
L'origine autorisee. Peut etre une origine spécifique ou * :
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Origin: *
Le wildcard * autorise toutes les origines. Pratique pour une API publique. Mais attention : * est incompatible avec les credentials (cookies, Authorization).
Access-Control-Allow-Methods
Les méthodes HTTP autorisees pour les requêtes cross-origin :
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH
Access-Control-Allow-Headers
Les headers custom que le client peut envoyer :
Access-Control-Allow-Headers: Content-Type, Authorization, X-Request-ID
Access-Control-Expose-Headers
Par défaut, le JavaScript ne peut lire que quelques headers de réponse (Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma). Pour exposer d'autres headers :
Access-Control-Expose-Headers: X-Total-Count, X-Request-ID
Sans ca, response.headers.get("X-Total-Count") renvoie null en JavaScript meme si le header est bien present dans la réponse.
Access-Control-Allow-Credentials
Pour envoyer des cookies ou des headers Authorization dans des requêtes cross-origin :
Access-Control-Allow-Credentials: true
Cote client, il faut aussi activer les credentials :
javascriptfetch("https://api.example.com/data", {
credentials: "include"
});
Et voici la restriction qui piège tout le monde : quand Access-Control-Allow-Credentials: true, le wildcard * est interdit dans Access-Control-Allow-Origin. Tu dois spécifier l'origine exacte. Meme chose pour Access-Control-Allow-Methods et Access-Control-Allow-Headers : pas de * avec les credentials.
Access-Control-Max-Age
Duree en secondes pendant laquelle le navigateur cache le résultat du preflight :
Access-Control-Max-Age: 86400
Chrome plafonne a 7200 secondes (2 heures) quoi que tu mettes.
Les erreurs CORS et comment les corriger
"No 'Access-Control-Allow-Origin' header is present"
Le serveur n'envoie pas le header. Ajoute-le dans la réponse. Verifie que tu l'ajoutes aussi sur les réponses d'erreur (404, 500), pas seulement les 200.
"The value of the 'Access-Control-Allow-Origin' header must not be the wildcard '*'"
Tu utilises * avec des credentials. Remplace par l'origine exacte du client. En pratique, tu lis le header Origin de la requête et tu le renvoies dans Access-Control-Allow-Origin apres avoir vérifié qu'il fait partie de ta liste blanche.
"Method PUT is not allowed"
Le preflight a reussi mais la méthode n'est pas dans Access-Control-Allow-Methods. Ajoute-la.
"Request header field Authorization is not allowed"
Pareil pour les headers. Ajoute Authorization dans Access-Control-Allow-Headers.
Le preflight renvoie un 404 ou 500
Ton serveur ne gere pas la méthode OPTIONS sur cette route. Ajoute un handler pour OPTIONS qui renvoie les headers CORS avec un 204.
Sur paltemps.fr, les headers CORS sont configures au niveau du reverse proxy. Ca centralise la configuration et évité d'oublier une route. Chaque erreur CORS que j'ai debuggee en production venait d'une route oubliee ou d'un middleware mal ordonne.
La configuration typique
Voici un middleware CORS minimal et correct :
javascriptfunction cors(req, res, next) {
const allowedOrigins = [
"https://app.example.com",
"https://staging.example.com"
];
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin);
res.setHeader("Access-Control-Allow-Credentials", "true");
res.setHeader("Vary", "Origin");
}
if (req.method === "OPTIONS") {
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
res.setHeader("Access-Control-Max-Age", "86400");
return res.status(204).end();
}
next();
}
Le Vary: Origin est crucial. Sans lui, un CDN pourrait cacher une réponse avec Access-Control-Allow-Origin: https://app.example.com et la servir a un autre client.
Résumé
- La same-origin policy bloque les requêtes cross-origin par défaut pour protéger l'utilisateur
- CORS permet au serveur d'autoriser des origines spécifiques via des headers
- Les requêtes simples passent directement, les autres declenchent un preflight OPTIONS
- Le wildcard
*est interdit avec les credentials Access-Control-Expose-Headersest nécessaire pour lire des headers custom en JavaScript- Les erreurs CORS se corrigent toujours cote serveur, jamais cote client
Vary: Originest obligatoire quand l'origine autorisee depend de la requête
Article précédent : 12 - L'authentification HTTP
Article suivant : 14 - Les redirections