
Chaque jour, un navigateur charge des pages qui dialoguent avec des dizaines de services : API, images, polices, cartes, solutions de paiement ou outils d’analyse. Pour que ces échanges ne deviennent pas une porte ouverte aux abus, les navigateurs appliquent un mécanisme de contrôle appelé CORS, pour Cross-Origin Resource Sharing. Souvent rencontré sous la forme d’une erreur dans la console, il joue pourtant un rôle central dans la sécurité du Web moderne.
CORS n’est pas un protocole réseau indépendant. C’est un ensemble de règles appliquées par le navigateur, en lien avec les réponses envoyées par les serveurs HTTP. Son objectif est simple : permettre à un site d’accéder à certaines ressources situées sur un autre domaine, mais uniquement lorsque le serveur distant l’autorise explicitement.
Pour comprendre CORS, il faut commencer par la Same-Origin Policy, ou politique de même origine. Cette règle historique des navigateurs empêche une page chargée depuis un site de lire librement les données d’un autre site. Une origine est définie par trois éléments : le schéma, comme HTTPS, le nom de domaine et le port. Ainsi, https://exemple.com et https://api.exemple.com sont deux origines différentes, tout comme http://exemple.com et https://exemple.com.
Cette restriction protège l’utilisateur. Sans elle, une page malveillante ouverte dans un onglet pourrait tenter de lire le contenu d’une messagerie, d’un espace bancaire ou d’un intranet déjà authentifié dans le même navigateur. Le navigateur enverrait éventuellement les cookies associés, mais il bloquerait la lecture de la réponse si l’origine n’est pas autorisée.
CORS vient nuancer cette règle. Il permet à un serveur de dire au navigateur : cette origine précise a le droit de consulter ma ressource. Cette logique repose sur des en-têtes HTTP standardisés, dont l’interprétation suit les règles définies dans les spécifications du Web, un domaine où les documents techniques comme les RFC dans les standards Internet jouent souvent un rôle de référence.
Lorsqu’une page JavaScript appelle une ressource située sur une autre origine, par exemple avec fetch ou XMLHttpRequest, le navigateur ajoute généralement un en-tête Origin à la requête. Cet en-tête indique l’origine de la page qui initie l’appel, par exemple : https://site-client.fr.
Le serveur distant peut alors répondre avec un en-tête Access-Control-Allow-Origin. Si sa valeur correspond à l’origine émettrice, le navigateur autorise le script à lire la réponse. Si cet en-tête est absent, ou s’il ne correspond pas, la requête peut avoir atteint le serveur, mais la réponse est rendue inaccessible au code JavaScript.
Ce point est souvent mal compris. CORS ne bloque pas nécessairement l’envoi de la requête au niveau réseau. Il bloque surtout l’accès au résultat côté navigateur. Le serveur peut donc voir une requête dans ses journaux même si, côté utilisateur, la console affiche une erreur CORS.
Le fonctionnement varie selon le type de requête. Les navigateurs distinguent les requêtes dites simples des requêtes plus sensibles. Une requête GET classique, sans en-têtes personnalisés et avec certains types de contenu seulement, peut être envoyée directement. Le navigateur vérifie ensuite la présence des bons en-têtes CORS dans la réponse.
En revanche, une requête qui utilise une méthode comme PUT, PATCH ou DELETE, ou qui ajoute un en-tête personnalisé tel que Authorization, déclenche souvent une étape préalable appelée preflight request. Le navigateur envoie alors une requête OPTIONS au serveur avant la vraie requête.
Cette requête de pré-vérification demande au serveur s’il accepte la méthode, les en-têtes et l’origine concernés. Le serveur doit répondre avec des en-têtes comme Access-Control-Allow-Methods et Access-Control-Allow-Headers. Si la réponse ne satisfait pas les conditions, la requête principale n’est pas exécutée par le navigateur.
Ce mécanisme rappelle que le Web repose sur plusieurs couches de négociation. Dans un autre registre, la négociation ALPN dans HTTP/2 illustre aussi la manière dont clients et serveurs s’accordent sur des paramètres avant d’échanger efficacement.
Plusieurs en-têtes déterminent le comportement CORS. Le plus connu est Access-Control-Allow-Origin. Il peut contenir une origine exacte, par exemple https://app.exemple.fr, ou l’astérisque *, qui signifie que la ressource est lisible par n’importe quelle origine. Cette seconde option est pratique pour des ressources publiques, mais inadaptée aux données sensibles.
L’en-tête Access-Control-Allow-Credentials sert à autoriser l’envoi et la lecture de réponses associées à des cookies, certificats client ou informations d’authentification HTTP. Il doit être utilisé avec prudence. Lorsque les credentials sont autorisés, l’astérisque n’est pas accepté pour Access-Control-Allow-Origin : le serveur doit renvoyer une origine explicite.
D’autres en-têtes complètent le dispositif. Access-Control-Expose-Headers indique quels en-têtes de réponse peuvent être lus par JavaScript. Access-Control-Max-Age permet de mettre en cache le résultat d’une requête preflight pendant une durée donnée, afin d’éviter des échanges OPTIONS répétés.
Comme pour les mécanismes de validation côté serveur, la précision compte. Dans le monde du courrier électronique, par exemple, un enregistrement SPF dans le DNS sert aussi à déclarer explicitement quelles sources sont légitimes, même si le contexte technique est différent.
Imaginons une application front-end hébergée sur https://app.boutique.fr qui interroge une API située sur https://api.boutique.fr. Même si les deux domaines appartiennent à la même entreprise, leurs origines diffèrent. Le navigateur applique donc CORS.
Si le script exécute une requête GET vers l’API, celle-ci devra répondre avec un en-tête tel que : Access-Control-Allow-Origin: https://app.boutique.fr. Le navigateur comparera cette valeur avec l’origine réelle de la page. Si elles correspondent, la réponse JSON pourra être lue et utilisée par l’application.
Supposons maintenant que l’application envoie une requête POST avec un jeton Authorization. Le navigateur déclenchera probablement une requête OPTIONS. L’API devra alors déclarer qu’elle accepte la méthode POST et l’en-tête Authorization. Sans cette autorisation, le code JavaScript recevra une erreur CORS avant même que la requête POST ne soit envoyée.
Dans les architectures modernes, cette configuration est fréquente : interface web séparée, API indépendante, CDN, authentification centralisée. CORS devient alors un élément de configuration aussi important que les certificats TLS, les règles de cache ou les paramètres d’authentification.
CORS est parfois présenté comme une barrière de sécurité côté serveur. C’est une simplification trompeuse. Le mécanisme est appliqué par les navigateurs. Un outil en ligne de commande, un script serveur ou un client HTTP personnalisé peut envoyer une requête à une API sans être soumis aux mêmes blocages d’affichage.
Il ne faut donc pas utiliser CORS comme unique protection d’une ressource privée. Une API doit continuer à vérifier l’identité de l’utilisateur, ses droits, la validité des jetons et la cohérence des données reçues. CORS complète ces contrôles, mais ne les remplace pas.
De la même manière, autoriser une origine ne signifie pas qu’elle est toujours sûre. Si le site autorisé contient une faille XSS, un attaquant peut exploiter cette origine légitime pour interagir avec l’API. La sécurité du Web dépend donc d’un ensemble cohérent de protections.
Cette distinction entre transport, autorisation et confiance existe ailleurs sur Internet. Le protocole SMTP face au spam montre bien qu’un mécanisme conçu pour transmettre des messages ne suffit pas, à lui seul, à établir la légitimité d’un expéditeur.
La première erreur consiste à utiliser Access-Control-Allow-Origin: * par facilité, y compris pour des ressources qui ne devraient pas être publiques. Cette configuration peut convenir à une police web ou à une API de données ouvertes, mais elle est risquée pour des informations liées à un compte utilisateur.
Une autre erreur fréquente est d’autoriser dynamiquement toute origine reçue dans l’en-tête Origin, sans liste blanche. Le serveur renvoie alors exactement l’origine demandée, ce qui donne l’illusion d’un contrôle tout en acceptant pratiquement n’importe quel site. Cette pratique est dangereuse, surtout avec les credentials activés.
Les développeurs rencontrent aussi des difficultés avec les environnements locaux. Une application sur http://localhost:3000 qui appelle une API sur http://localhost:8080 change bien d’origine, car le port diffère. Le blocage est donc normal. La bonne solution consiste à configurer explicitement l’origine de développement, ou à utiliser un proxy local adapté.
Enfin, il ne faut pas confondre erreur CORS et panne serveur. Une réponse HTTP 500, une redirection mal gérée ou un certificat invalide peuvent se traduire par des messages confus dans la console. L’analyse doit toujours croiser les journaux serveur, l’onglet réseau du navigateur et les en-têtes réellement échangés.
Une configuration robuste commence par une liste claire des origines autorisées. Pour une application en production, mieux vaut déclarer précisément https://app.exemple.fr que recourir à un joker global. Les environnements de développement, de test et de production doivent être séparés, chacun avec ses propres règles.
Il est également recommandé de limiter les méthodes et les en-têtes aux besoins réels. Si une API n’utilise que GET et POST, inutile d’autoriser DELETE. Si seul l’en-tête Authorization est nécessaire, il n’est pas prudent d’ouvrir largement tous les en-têtes possibles.
La mise en cache des pré-vérifications peut améliorer les performances, mais elle doit rester raisonnable. Un Access-Control-Max-Age trop long peut compliquer le déploiement rapide d’un changement de politique. Comme souvent, l’équilibre dépend du contexte : fréquence des appels, sensibilité des données, architecture applicative.
Enfin, CORS doit être pensé comme une pièce d’un système plus large. Les API circulent sur des réseaux interconnectés, où routage, DNS, TLS et politiques applicatives se combinent. À une échelle différente, le protocole BGP entre réseaux autonomes rappelle que la circulation des données sur Internet repose elle aussi sur des décisions de confiance et de contrôle.
Bien configuré, CORS rend possible un Web plus modulaire, où les applications peuvent dialoguer avec des services distants sans abandonner les protections fondamentales du navigateur. Mal compris, il devient une source d’erreurs frustrantes ou de fausses garanties. Sa logique reste pourtant accessible : le navigateur demande, le serveur autorise, et l’accès à la réponse dépend de cette autorisation explicite.