CORS ne protège pas votre API les ami(e)s codeurs. Et comprendre pourquoi, vraiment comprendre, pas juste accepter l'affirmation, ça change la façon dont tu penses à la sécurité de ce que tu construis.

Le raisonnement habituel qu’on entend souvent ressemble à "Eh ! Mon API renvoie Access-Control-Allow-Origin: https://mon-app.com. Du coup, seuls les appels venant de mon-app.com peuvent accéder à mes données. Mon API est protégée. Youpi !"

Bah ouais c'est logique. C'est on ne peut plus cohérent. Mais c'est faux. Pas juste partiellement faux. Non, totalement. Pas faux dans le sens vous avez fait une erreur de configuration. Faux dans le sens vous avez carrément mal compris à quoi sert le mécanisme.

Et pour voir pourquoi, il faut aller là où personne ne va spontanément. Dans les specs.

CORS est défini dans ce que l’on nomme le Fetch Living Standard du WHATWG. C’est en fait la même spec qui définit comment fonctionne par exemple l’api fetch() dans votre navigateur les ami(e)s codeurs.

Dans la section consacrée au protocole CORS, la spec dit explicitement "This section explains the CORS protocol as it pertains to server developers. Requirements for user agents are part of the fetch algorithm."

Traduit par votre serviteur bilingue, ce que fait CORS côté serveur, a savoir renvoyer les bons headers, ca s’apparente en fait à de la communication. Ce qui applique réellement les restrictions, c'est ce que l’on nomme le user agent. C'est-à-dire votre navigateur. Absolument pas votre serveur.

Cette distinction, elle n’est pas anecdotique. Il faut bien la saisir les ami(e)s codeurs. Car elle est la clé de tout le reste.

Et si on s’aventure ailleurs, jusqu'au RFC 6454 portant le nom de "The Web Origin Concept", le résumé de ce qu’on vient de se dire est même énoncé dès le départ "This document defines the concept of an 'origin', which is often used as the scope of authority or privilege by user agents. Typically, user agents isolate content retrieved from different origins to prevent malicious web site operators from interfering with the operation of benign web sites.".

Le mot "user agents" apparaît même deux fois en deux phrases. Ce n'est pas un hasard. C'est le sujet. Et c'est l'acteur central du mécanisme finalement. CORS est en fait un mécanisme de navigateur. Il a été conçu pour protéger les personnes qui utilisent les navigateurs. Pas pour protéger les serveurs qui répondent aux requêtes, encore une fois.

Ouvrez un terminal les ami(e)s codeurs. Prenez n'importe quelle API "protégée" par CORS que vous avez sous la main. Et lancez une commande curl avec un header Origin.

curl https://ton-api.com/utilisateurs/42 \
  -H "Origin: https://site-malveillant.com"

Etrange hein ? … Avouez le, vous avez obtenu une réponse ? Complète en plus. Avec toutes les données ! Peu importe ce que vous avez mis dans Access-Control-Allow-Origin.

C’est bien parce que curl n'est pas un navigateur. curl ne vérifie pas les headers CORS. curl s'en fiche, mais alors complètement ! Et c'est pareil pour Python avec requests, pour Node.js avec axios, pour un script PHP, pour n'importe quel client HTTP qui ne soit pas un navigateur web.

Mais ce n'est pas tout. Parce que même dans un navigateur, la situation est plus subtile qu'elle n'y paraît. La Fetch Living Standard dont on parlait plus haut distingue en fait deux types de requêtes CORS. Ce qu’elle appelle les simple requests et les requêtes qui déclenchent un preflight. Une "simple request", c'est finalement juste par exemple un GET ou un POST avec un Content-Type standard (application/x-www-form-urlencoded, multipart/form-data ou text/plain).

On retrouve ce concept de requêtes simples directement dans la documentation MDN de CORS, et elle dit quelque chose de très important "The motivation is that the <form> element from HTML 4.0 (which predates cross-site fetch() and XMLHttpRequest) can submit simple requests to any origin, so anyone writing a server must already be protecting against cross-site request forgery (CSRF).".

C’est un passage un peu lugubre avec tout ce jargon technique mais je pense que ça mérite qu'on s'y arrête les ami(e)s codeurs. Pour les requêtes simples, le navigateur envoie la requête au serveur AVANT de vérifier les headers CORS. Le serveur la reçoit. Le serveur l'exécute. Et CORS ne va intervenir qu'au moment de décider si le JavaScript dans le navigateur a le droit de lire la réponse.

Autrement dit, pour les requêtes simples, CORS ne protège pas votre serveur d'une exécution non désirée. Il protège le résultat de cette exécution d'une lecture non désirée par un script malveillant par exemple.

Ce n'est pas du tout la même chose. Ca reste assez subtil je le conçois. Mais c’est différent.

Pour comprendre ensemble ce que CORS fait vraiment, je pense également que c’est intéressant et qu’il faut remettre le mécanisme dans son contexte historique.

Après mes petites recherches, le support du cross-origin a été proposé initialement en mars 2004 par les petits gars que sont Matt Oshry, Brad Porter et Michael Bodell de Tellme Networks, à l'origine pour VoiceXML 2.1. L'idée, c’était apparemment de permettre à des navigateurs de faire des requêtes vers d'autres origines en toute sécurité. Le mécanisme a ensuite été généralisé et formalisé plus tard par le W3C, avant que le WHATWG ne prenne le relais avec le Fetch Living Standard que l’on a vu tout a l’heure.

En fait, le problème que CORS cherchait à résoudre était le suivant. Avant XMLHttpRequest et fetch(), un site malveillant ne pouvait pas facilement lire les données d'un autre site. Mais avec l'apparition de JavaScript et des APIs de requêtes asynchrones, un scénario devenait possible.

Ah …. cette évolution technologique me direz vous…

Imaginez que vous êtes connecté à votre banque sur banque.fr. Vous allez ensuite visiter site-malveillant.com. Ce site malveillant va exécuté du JavaScript qui fait une requête vers banque.fr/api/solde. Sans aucune protection, votre navigateur enverrait cette requête avec vos cookies de session. Et le script malveillant pourrait lire votre solde bancaire.

C'est ce scénario que CORS protège ! Il empêche un script sur site-malveillant.com de lire la réponse d'une requête vers banque.fr, à moins que banque.fr ne dise explicitement que c'est autorisé.

La personne protégée ici, c'est toi, l’ami codeur ! C’est l'utilisateur. Pas le serveur de la banque. Le serveur de la banque reçoit la requête de toute façon. Ce que CORS contrôle, c'est ce que le script malveillant dans ton navigateur peut faire avec la réponse.

Un token JWT, une session côté serveur, une clé API, c'est ce qui dit à ton serveur "cette requête vient de quelqu'un qui a le droit de faire ça". Peu importe l'origine de la requête, peu importe si c'est curl ou un navigateur. Sans credentials valides, la requête est refusée. C'est ça, protéger votre API.

CORS n'est pas inutile. Il fait exactement ce pour quoi il a été conçu finalement.

Il permet à votre frontend hébergé sur app.mon-domaine.com de faire des requêtes vers api.mon-domaine.com ou vers des APIs tierces qui t’autorisent explicitement. Sans CORS, la Same-Origin Policy bloquerait ces requêtes par défaut et le navigateur refuserait de laisser le JavaScript lire les réponses cross-origin.

Et en fait, mal configuré, CORS peut même créer des vulnérabilités ! Si vous mettez Access-Control-Allow-Origin: * avec Access-Control-Allow-Credentials: true, vous ouvrez potentiellement la porte à des fuites de données sensibles pour vos utilisateurs connectés. Un Access-Control-Allow-Origin qui reflète dynamiquement n'importe quelle origine sans validation, c'est une faille réelle, connu, documentée, et surtout exploitée.

CORS fait donc un travail important. Mais ce travail, c'est protéger les utilisateurs de navigateurs contre des scripts malveillants qui voudraient lire leurs données cross-origin. Pas protéger votre serveur contre des appels non autorisés.

Mais ca pose une question finalement les ami(e)s codeurs…

Si quelqu'un peut appeler votre API avec curl sans aucune restriction, c'est une question de design, pas de CORS. Est-ce que c'est voulu ? Est-ce une API publique ? Est-ce qu'elle expose des données sensibles sans authentification ?

Ce sont des questions d'architecture, pas des questions de headers.

Et c'est là que ça devient intéressant, parce que beaucoup de systèmes sont construits sur l'hypothèse implicite que "CORS bloque les accès non autorisés" et il y a cette sensation en lisant cette phrase que ça libère mentalement du soin à apporter à l'authentification. Mais C'est l'inverse qu'il faudrait faire en fait ! Construire une API comme si CORS n'existait pas, comme si n'importe quel client pouvait appeler n'importe quel endpoint, et se demander à chaque endpoint : "est-ce que cette route est correctement authentifiée ? est-ce que je valide les permissions ?"

Que l’on se comprenne bien les ami(e)s codeurs avant de se quitter. Je n'ai pas dit que CORS était mal conçu. Il est bien conçu pour ce qu'il fait. Le problème n'est pas le mécanisme, c'est la confusion sur ce que ce mécanisme protège.

CORS est une sécurité pour tes utilisateurs, pas pour ton serveur. Ces deux phrases ne s'excluent pas, elles se complètent. Mais les confondre, c'est rater toute une couche de la vraie sécurité de ce que tu construis.

Keep Reading