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.