HTTP en profondeur - 13 - CORS : les requêtes cross-origin demystifiees

Same-origin policy, preflight, Access-Control-Allow-Origin et tous les headers CORS expliques avec les erreurs courantes et leurs solutions.

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-data ou text/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-Headers est nécessaire pour lire des headers custom en JavaScript
  • Les erreurs CORS se corrigent toujours cote serveur, jamais cote client
  • Vary: Origin est obligatoire quand l'origine autorisee depend de la requête

Article précédent : 12 - L'authentification HTTP

Article suivant : 14 - Les redirections

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.