Mystères ésotériques : la vérité sur le CORS

La plupart des développeurs web se sont déjà retrouvés face à une erreur liée au CORS. Il est courant de se retrouver confronté à un en-tête CORS Access-Control-Allow-Origin missing ou CORS request did not succeed.
Dans tous les cas, le problème vient du CORS ou Cross Origin Resource Sharing.

Cet article expliquera ce qu'est le CORS, pourquoi cela est présent dans le web moderne et quelles sont les bonnes pratiques à adopter.

Le CORS en bref

Le Cross Origin Resource Sharing est un système se servant de Headers HTTP pour définir les règles de partage de contenu entre différentes origins.
Dans le cas du CORS, une origin revient au domain, au subdomain et au port s’il est spécifié :

Exemple d'origin

Si l'origin est la même, toutes les requêtes HTTP peuvent se faire par défaut, car le navigateur considère que le propriétaire de l'origin est la même personne (même si cela n'est pas toujours vrai).

Dans le cas où l’émetteur de la requête HTTP n'a pas la même origin que le serveur qui la traite, le CORS entre en jeu.

Prenons le cas où https://fr.ippon.tech/ fait une requête HTTP GET sur http://amazing-api/docs. Le navigateur vérifiera les en-têtes de la réponse et si l'en-tête Access-Control-Allow-Origin ne comprend pas https://fr.ippon.tech/ la requête sera en échec.

La plupart des erreurs de CORS viennent de l'Access-Control-Allow-Origin header qui n'est pas configuré par le serveur.

Pourquoi le CORS

Un peu d'histoire

Le CORS a fait son apparition dans le web et dans nos navigateurs en Septembre 1995 avec la parution du navigateur Netscape Navigator 2.
Ce dernier a amené plusieurs avancées importantes dans le web, notamment les HTML Frames qui sont des composants d'une page web qui peuvent afficher et charger du contenu indépendamment de la page web dans laquelle la frame est située.
Un autre ajout de Netscape 2 est le support de LiveScript, qui est devenu JavaScript.

Évidemment, Netscape s'est rendu compte qu'un langage de script et des frames pouvant exécuter et charger du contenu indépendamment de son contenant était un dangereux mélange pour la sécurité des utilisateurs.
Pour éviter cela un mécanisme pour gérer le partage de contenu arrive avec Netscape 2 : le CORS qui régit les échanges entre origin différentes.

De nos jours

Le CORS est un système mis en place il y a presque 30 ans. Depuis les années 1995 la consommation et la création d'applications web ont énormément évolué. Toutefois, le CORS est toujours là à causer des erreurs dans nos navigateurs, et ce n'est pas pour rien.

Si les HTML Frames sont désormais considérées obsolètes et que leur utilisation a été retirée des standards HTML5, le CORS reste pertinent quand le web est en grande partie composé de web-services utilisant le protocole HTTP.

De nos jours le CORS est un moyen d'encadrer les communications HTTP entre services différents. Les APIs Fetch et XMLHttpRequest, qui sont les deux APIs de base pour les requêtes HTTP sur les navigateurs récents, envoient par défaut l’en-tête Origin qui va notifier le serveur que cette requête HTTP est une requête CORS. Cela permet de diminuer le risque de Cross-Site-Request-Forgery (CSRF)

Le CORS en détail

CORS ou pas CORS

Une requête HTTP n'est pas forcément régie par les règles CORS. Les APIs que l'on utilise pour du développement côté client l'incorporent par défaut. Mais ce ne sont pas les seules requêtes exécutées par le navigateur. Du code HTML est également capable d'effectuer des requêtes cross-origin :
<img src="..." /> ou <link rel="stylesheet" href="..." /> peuvent charger du contenu d'autres origin mais ne font pas de requête CORS. De manière générale, les balises HTML ne font pas de requêtes CORS.

Toutefois, certaines balises HTML font quand même des requêtes CORS ; c'est le cas des scripts de type module.
<script type="module" src="..."></script>
Il n'y a pas de règles générales sur quels éléments HTML font des requêtes CORS. Mais il est possible d'utiliser l'attribut HTML crossorigin pour forcer un élément HTML à effectuer des requêtes cross-origin.

Pour savoir si une requête est CORS ou non, on peut se servir des outils développeurs du navigateur et regarder l'en-tête Sec-Fetch-Mode qui va prendre la valeur cors ou no-cors.

Exemple de requêtes simple

Reprenons l'exemple ou https://fr.ippon.tech/ fait une requête HTTP GET sur http://amazing-api/docs. Le code côté navigateur fait une simple requête et le navigateur va ajouter plusieurs en-tête automatiquement. Parmi ceux-là, certains sont liés au CORS.
L’en-tête Origin contient l'origin de la requête. La présence de cet en-tête signifie également que l'initiateur de la requête, dans ce cas le navigateur, s'attend à ce que l'ensemble de l'échange HTTP respecte le mécanisme CORS.

L’en-tête Host est le domaine qui reçoit la requête du client. Le navigateur s’attend à ce que ce soit ce domaine qui émettent la réponse.

Maintenant, c'est au serveur de construire la réponse à la requête du client. Il doit rajouter certains en-têtes à sa réponse. Le seul obligatoire est Access-Control-Allow-Origin, qui doit contenir l'origin de la requête. Dans notre exemple Access-Control-Allow-Origin doit avoir le caractère générique * qui signifie que toutes les origins sont acceptées ou alors https://fr.ippon.tech.

Les en-têtes Access-Control-Allow-Origin suivantes ne marchent pas dans notre cas :

https://fr.IPPON.tech L'en-tête est sensible à la casse.
https://fr.ippon.tech, https://another-site.fr Une seule valeur est autorisée dans l'en-tête.
https://fr.ippon.* Le wildcard * fonctionne uniquement seul.

Dans le cas d'une erreur CORS la réponse ne sera pas traitée par le navigateur, mais aura bien été envoyée au client. Et peut être visible dans les DevTools du navigateur. Cela est possible, car le système CORS considère notre requête comme normale.

Preflight Requests

Quand une requête est jugée anormale par le navigateur, une preflight request est envoyée. Ce sont les requêtes OPTIONS que l'on peut voir dans les outils développeur du navigateur. Elles servent à vérifier le bon respect du CORS avant l'envoi de la vraie requête.

La catégorisation comme anormale d'une requête est assez abstraite, voire arbitraire, et varie entre chaque navigateur. De manière générale, les requêtes qui contiennent un en-tête qui n'est pas dans les en-têtes courants sont systématiquement classées comme anormales. La plupart des requêtes POST , PUT et DELETE ont le droit à des preflight requests.

Dans le cas d'une preflight request, de nouveaux en-têtes CORS apparaissent. Il y a toujours les en-têtes Origin et Host, mais on a également l'en-tête Access-Control-Request-Method qui contient le verbe HTTP de la requête qui va suivre. Il y a aussi l'en-tête Access-Control-Request-Headers qui contient les en-têtes non courants de la future requête. S’il y a plus d'un en-tête ils seront séparés par , .

Le serveur va traiter les en-têtes de la requête OPTIONS par rapport aux siens. On retrouve du côté serveur l’en-tête Access-Control-Allow-Method qui est le pendant de l'en-tête Access-Control-Request-Method et qui contient la liste des verbes HTTP autorisés.
L'en-tête Access-Control-Allow-Headers contient la liste des headers autorisés par le serveur. L'en-tête Access-Control-Max-Age est une indication, en secondes, de la durée de la validité des informations renvoyées par le serveur.

Si les en-têtes dans la preflight request sont corrects, la requête principale est envoyée. Dans le cas où cela ne convient pas, une erreur CORS apparaîtra dans les outils développeur du navigateur.

Gestion de Credentials

Le mécanisme CORS incorpore également un système de gestion de credentials (identités). Dans le cas du CORS, les credentials regroupent les cookies, les en-têtes Authorization ainsi que les certificats TLS client. Il est possible d'échanger des requêtes CORS avec credentials, avec ou sans preflight requests.
La requête du client doit être configurée pour autoriser les credentials.

Avec l'API fetch :

fetch(url, {
  credentials: 'include'
})

La réponse doit alors contenir l'en-tête Access-Control-Allow-Credentials avec la valeur true. Elle doit également contenir l'en-tête Access-Control-Allow-Origin : https://fr.ippon.tech car le caractère générique * est désactivé.

Erreurs Courantes

Il existe plusieurs erreurs CORS courantes. Voici les trois plus communes ainsi que la façon de les régler :

  • CORS header 'Access-Control-Allow-Origin' missing
    Cette erreur vient du fait que la réponse du serveur ne contient pas l’en-tête. Il est impossible de régler cette erreur si on ne maîtrise pas le serveur. Il faut modifier le serveur pour inclure l’en-tête. La façon de le faire varie en fonction de la technologie du serveur.

  • Reason: CORS header ‘Access-Control-Allow-Origin’ does not match ‘xyz’
    Cette erreur est due à l’en-tête qui ne correspond pas à l’origin du client. Il faut pour cela modifier l’en-tête pour contenir l’origin du client. Si on ne contrôle pas le serveur, cela peut être tout à fait normal.

  • Did not find method in CORS header 'Access-Control-Allow-Methods'
    Cela se produit quand on exécute une requête avec un verbe HTTP non autorisé par le serveur. Si on a le contrôle du serveur, on peut modifier l’en-tête.

Les bonnes pratiques

Il existe plusieurs bonnes pratiques à respecter pour maximiser la protection CORS sur ses applications web aussi bien côté client que côté serveur.
Il est totalement possible pour un serveur de toujours retourner l'en-tête Access-Control-Allow-Origin : * si le but de l'application est de diffuser du contenu le plus largement possible.
Si la ressource renvoie parfois des données personnelles en fonction des cookies il est aussi possible de laisser l'en-tête Access-Control-Allow-Origin : * à condition d'également mettre l'en-tête Vary : Cookie .

Dans le cas d'une API traitant des données privées, étant destinées à servir une seule origin il faut configurer l'en-tête Access-Control-Allow-Origin pour contenir l'origin de notre client.

Maintenant, si l'on veut que notre serveur soit accessible de plusieurs origin comme par exemple : https://mobile.my-website , https://my-website , https://tablet.my-website, on ne peut pas retourner plusieurs valeurs dans l'en-tête Access-Control-Allow-Origin. Il faut donc analyser programmatiquement l'en-tête Origin de la requête pour construire la réponse. Il y a deux possibilités recommandées :

  • Utilisation d'une whitelist écrite en dur ou stockée en base de données contenant les origins autorisées. Dans ce cas, il faut vérifier si l'origin de la requête concorde avec une de la whitelist
  • Utilisation d'une expression régulière, ce qui peut être utile pour accepter tous les sous-domaines.

Dans ce cas-là, l'en-tête Access-Control-Allow-Origin de la réponse variera en fonction de l'en-tête Origin de la requête. Il ne faut surtout pas oublier de rajouter l'en-tête Vary : Origin à la réponse pour éviter que le cache ne cause des soucis.

Il faut également noter que le CORS n'est pas là pour sécuriser un serveur et qu'il est très simple de modifier l'en-tête Origin pour tromper un serveur.

Questions courantes sur le CORS

Pourquoi ça marche sur Postman mais pas sur mon navigateur ?

Par défaut Postman n'envoie pas d'en-tête Origin ce qui fait que la requête n'est pas sujette au CORS.

Pourquoi le résultat de la requête HTTP est chargé mais pas affiché ?

Même si la requête cause une erreur CORS, le résultat est quand même récupéré par le navigateur, mais il ne sera pas chargé dans le HTML pour éviter de potentielles vulnérabilités.

Conclusion

Le CORS est un système important dans le web. Comprendre ce mécanisme est important et peut faire gagner beaucoup de temps. Cet article propose une introduction au CORS, mais si vous souhaitez mieux comprendre comment les navigateurs gèrent le CORS, ainsi que plus d’informations pour créer des APIs se servant au mieux du CORS, je vous conseille le livre CORS IN ACTION.

Il existe également un mécanisme similaire au CORS appelé CSP qui permet de sécuriser ses applications web contre le Cross-Site-Scripting. Il n’est pas activé par défaut sur les navigateurs mais, il peut être paramétré par le serveur.