Deux heures du matin. Vous avez lancé Create React App en local, le frontend tourne sur le port 3000, l’API backend sur le port 8000. Vous écrivez votre premier appel fetch(), et la console de Chrome affiche une ligne rouge : « has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. » Vous ouvrez Google, tapez « CORS error fix », le premier résultat Stack Overflow vous dit « ajoutez Access-Control-Allow-Origin: * côté serveur ». Vous obtempérez, la ligne rouge disparaît, le monde retrouve la paix. Quant à savoir ce que fait exactement cette configuration, pourquoi un header côté serveur contrôle le comportement côté navigateur — vous n’êtes pas tout à fait sûr, et vous n’avez pas très envie d’approfondir. Après tout, le code fonctionne. C’est une scène qui se produit chaque seconde sur cette planète. Vingt ans plus tard, CORS reste le mécanisme de sécurité le plus facilement « réparé sans être compris » du développement Web. En juin 2026, un vieil article de 2019 intitulé « Developers don’t understand CORS » a refait surface sur Hacker News, atteignant 353 points et 251 commentaires. Les deux commentaires les plus votés sont en opposition directe, et le fil de discussion en dessous s’étend sur deux cents couches, chaque camp citant abondamment ses sources sans jamais convaincre l’autre. Plus révélateur encore : la cible des critiques est précisément cet article de vulgarisation lui-même — « Even TFA (The F*ing Article) seemingly doesn’t understand CORS. » L’auteur qui écrit pour expliquer CORS se trompe lui-même dans ses explications.
Ce que fait réellement CORS
Pour comprendre cette querelle, il faut revenir à la question la plus fondamentale : à quoi sert CORS ? Le tableau que j’ai reconstitué à partir de la littérature technique et des spécifications est à peu près le suivant : CORS (Cross-Origin Resource Sharing, partage de ressources entre origines) est un protocole implémenté par les navigateurs pour assouplir, sous certaines conditions, les restrictions de la Same-Origin Policy (SOP). Assouplir, pas renforcer. La SOP est le socle de sécurité intégré aux navigateurs : par défaut, le JavaScript chargé depuis example.com ne peut pas envoyer de requête à bank.com et en lire la réponse. Cette politique par défaut protège l’utilisateur — si vous êtes connecté à votre banque en ligne et que le navigateur conserve vos cookies d’authentification, aucun autre site que vous visitez ne peut lire vos données bancaires en secret. Le rôle de CORS est de donner au serveur un mécanisme pour dire « certaines autres origines peuvent lire mes réponses », via des en-têtes de réponse comme Access-Control-Allow-Origin.
Le nom lui-même trahit sa fonction : il s’agit de partage (Sharing), pas de blocage. Mais ce nom est précisément à l’origine d’une confusion massive — quand un développeur voit l’erreur de console « blocked by CORS », sa réaction instinctive est « CORS m’empêche de faire quelque chose ». En réalité, ce qui bloque, c’est la SOP ; CORS est le mécanisme par lequel le navigateur vérifie si le serveur autorise le partage lorsque vous tentez une requête cross-origin. Si le serveur n’a pas accordé d’autorisation, le comportement du navigateur est « interdiction de lire la réponse » (et pour les requêtes non simples, interdiction même d’envoyer la requête). Mais ce « non » est attribué au nom « CORS ». Le décalage entre le nom et le comportement est le premier versement dans le capital de la dette cognitive.
Deux versions de « vrai », deux cents couches de bataille
La divergence centrale de la discussion HN peut se résumer à l’affrontement entre deux commentaires. Le premier, de l’utilisateur muvlon (le plus voté, posté 17 heures auparavant) dit en substance : cet article lui-même n’a pas compris CORS — CORS n’empêche pas les requêtes, il ne fait qu’assouplir les restrictions par défaut. Le JavaScript de n’importe quel site peut envoyer des requêtes à votre localhost:19421. L’en-tête Access-Control-Allow-Origin ne fait que déterminer si la réponse peut être lue ou non ; la requête, elle, est envoyée quoi qu’il arrive. Le second, de l’utilisateur stymaar (12 heures auparavant), réplique directement : non, ce que vous dites est faux — pour les méthodes sûres comme GET, la requête est effectivement envoyée, mais GET est censé être idempotent, et l’impossibilité de lire la réponse constitue la totalité de la protection. Pour les requêtes non idempotentes, le navigateur émet d’abord une requête OPTIONS preflight ; si la réponse preflight ne contient pas les bons en-têtes CORS, le navigateur n’envoie tout simplement pas la requête réelle.
Aucun des deux ne raconte n’importe quoi. Chacun a raison dans le scénario qu’il a en tête. Le scénario de muvlon couvre les « requêtes simples » (simple requests) — celles qui ne déclenchent pas de preflight : GET, HEAD, POST (avec Content-Type application/x-www-form-urlencoded, multipart/form-data ou text/plain), ainsi qu’un ensemble d’en-têtes standard considérés comme sûrs. Ces requêtes sont envoyées, le serveur les traite, la réponse revient, le navigateur ne la délivre simplement pas au JavaScript. Stymaar décrit les « requêtes non simples » — PUT, DELETE, PATCH, POST avec Content-Type application/json, requêtes avec en-tête Authorization, etc. Ces requêtes déclenchent d’abord un preflight OPTIONS ; si le preflight échoue, la requête réelle n’est jamais émise.
Le jugement d’ingénierie est le suivant : chaque camp a raison dans son propre contexte, mais chacun a érigé sa vérité contextuelle en vérité universelle. La phrase de muvlon « the requests happen in any case » est fausse comme affirmation universelle — pour les requêtes non simples, l’échec du preflight empêche bel et bien l’envoi de la requête. La défense de l’article original par stymaar via le mécanisme de preflight a aussi ses lacunes — il omet que le scénario Zoom implique un serveur localhost local, que la surface d’attaque provient de requêtes simples de type GET, et que la formulation de l’article original « only Javascript running on the zoom.us domain can talk to the localhost webserver » est effectivement imprécise : n’importe quel site peut « parler à » ce serveur localhost (envoyer des requêtes simples), seuls les sites autorisés peuvent lire la réponse. Si le serveur localhost expose des opérations dangereuses sur des endpoints GET, Access-Control-Allow-Origin n’empêche pas la requête d’arriver, il empêche seulement la réponse d’être lue. Et une requête GET destructive, une fois envoyée, est envoyée.
Le calcul infinitésimal du preflight
Le mécanisme de preflight recèle lui-même des détails supplémentaires faciles à manquer. Dans la discussion HN, quelqu’un a souligné qu’une requête POST avec Content-Type text/plain contourne le preflight — parce que text/plain figure dans la liste blanche des « requêtes simples ». Un attaquant peut construire un formulaire comme celui-ci :
<form action="https://victime.com/api" method="POST" enctype="text/plain">
<input name='{"clé":"valeur", "ignore":"' value='"}'>
</form>
Le contenu envoyé au serveur sera {"clé":"valeur", "ignore":"="}, qui ressemble à du JSON malformé, mais si le backend ne vérifie pas strictement l’en-tête Content-Type avant d’appeler JSON.parse sur le body, cette requête peut traverser la barrière du preflight. Un utilisateur affirmant avoir exploité cette technique avec succès lors de multiples tests d’intrusion s’est manifesté dans les commentaires. Ce n’est pas une simple spéculation théorique — si le serveur ne valide pas le Content-Type, un POST simple en text/plain ou multipart/form-data peut transporter une charge arbitraire. De même, si la vérification du Content-Type se fait par correspondance de préfixe plutôt que par correspondance exacte, une valeur d’en-tête comme multipart/form-data; boundary=application/json peut aussi passer au travers.
Ces cas marginaux illustrent une réalité : le modèle de sécurité de CORS ne peut être simplifié ni en « la requête peut-elle atteindre le serveur ? » ni en « la réponse peut-elle être lue ? » — c’est un arbre de décision bifurquant, où les requêtes simples et non simples empruntent des chemins différents, avec des périmètres de protection différents sur chaque chemin. Généraliser la règle d’un chemin particulier en règle universelle produit inévitablement un biais cognitif. Et cet arbre de décision ne cesse de se ramifier — les en-têtes Sec-Fetch-*, l’attribut de cookie SameSite, Cross-Origin-Embedder-Policy, Cross-Origin-Opener-Policy : chaque couche ajoute de nouvelles sémantiques par-dessus CORS, rendant un modèle mental déjà complexe encore plus difficile à maîtriser.
Pourquoi même les auteurs d’articles de vulgarisation se trompent
L’article original de Chris Foster, publié en juillet 2019, s’articule autour du cas de la vulnérabilité du serveur web local de Zoom. Zoom exécutait sur la machine de l’utilisateur un serveur web écoutant sur localhost:19421 : lorsqu’un utilisateur cliquait sur un lien Zoom, la page web envoyait une requête à ce serveur local pour ouvrir le client natif. Pour contourner CORS, Zoom n’utilisait pas AJAX mais chargeait une image, encodant le code de statut dans les dimensions de l’image. La recommandation de Foster : ce serveur web local devrait définir Access-Control-Allow-Origin: https://zoom.us, afin que « seul le JavaScript exécuté sur le domaine zoom.us puisse communiquer avec le serveur local ».
La première moitié du jugement de Foster (l’approche de Zoom n’est pas sûre) est correcte, mais la seconde moitié de sa formulation (« seul le JavaScript de zoom.us peut communiquer ») est techniquement ambiguë. Strictement parlant, Access-Control-Allow-Origin n’empêche pas d’autres sites d’envoyer des requêtes simples à localhost ; il empêche seulement le JavaScript des autres sites de lire la réponse. Si le serveur web local expose des opérations sensibles sur des endpoints GET, l’en-tête CORS seul ne suffit pas.
Mais Foster n’est pas le seul fautif. L’intégralité du fil de commentaires HN est consacrée à discourir sur CORS de diverses manières, et les désaccords entre les commentateurs ne sont pas moins profonds que leurs désaccords avec Foster. L’un insiste que CORS ne bloque absolument aucune requête ; un autre rétorque que le preflight est précisément conçu pour bloquer les requêtes ; un troisième intervient pour souligner le problème du contournement du preflight par POST text/plain et les formulaires ; un quatrième ajoute que même si la requête est envoyée, sans en-tête CORS la réponse est illisible, donc la protection est complète pour les opérations GET — tant que le serveur n’expose pas d’opérations d’écriture sur GET. Chaque couche de réfutation expose l’incomplétude de la couche précédente, et le résultat final est que deux cent cinquante commentaires plus tard, aucun consensus ne s’est dégagé.
J’observe un schéma : la difficulté cognitive de CORS ne vient pas seulement de sa complexité, mais du fait qu’elle exige du développeur qu’il comprenne simultanément trois choses pour modéliser correctement le système — le socle SOP du navigateur, CORS comme mécanisme d’assouplissement de la SOP, et les conventions de sécurité et d’idempotence des méthodes HTTP. Ces trois choses appartiennent respectivement à trois domaines : l’architecture des navigateurs, les protocoles de sécurité Web et la conception RESTful — la plupart des développeurs n’en maîtrisent qu’un ou deux. Quand on modélise uniquement avec « SOP + CORS », on aboutit facilement à la conclusion que « la requête a été bloquée » (parce que l’effet global au niveau du navigateur y ressemble). Quand on modélise uniquement avec la « sémantique HTTP », on voit que le serveur a reçu la requête et renvoyé une réponse — « la requête a bien été envoyée ». Les deux modélisations sont correctes à leurs niveaux respectifs, mais leur projection sur le même terme « CORS » crée un conflit.
La fissure générationnelle
Une observation dans les commentaires est particulièrement intéressante : c’est peut-être un problème générationnel. Si vous avez commencé le développement Web avant l’existence de CORS, vous avez connu l’époque où seule la SOP existait, sans requête cross-origin légitime. Vous savez comment JSONP était bricolé, vous savez pourquoi les balises <img> et <script> peuvent faire du cross-origin alors que XHR ne le peut pas. Quand CORS est apparu, vous avez vu une porte s’ouvrir dans la SOP — c’était une solution. Mais si vous avez commencé à écrire des applications Web après l’existence de CORS, la première erreur cross-origin que vous avez rencontrée disait « blocked by CORS », et votre instinct vous disait que CORS vous empêchait de travailler — c’était un problème.
La différence générationnelle existe bel et bien, mais le problème plus profond est que la documentation, l’enseignement et les messages d’erreur de CORS sont intrinsèquement biaisés dans leur conception. Le message d’erreur de la console du navigateur dit « blocked by CORS », et non « blocked by Same-Origin Policy due to missing CORS authorization ». La documentation MDN explique le mécanisme complet, mais la plupart des développeurs ne lisent pas la documentation complète — ils s’arrêtent à la première réponse Stack Overflow qui résout leur problème. Dans les commentaires HN, plus d’une personne a admis : « Chaque fois que je rencontre un problème CORS, je dois le réapprendre entièrement, puis je l’oublie de nouveau. » Un commentateur se présentant comme CTO a déclaré que les utilisateurs de son entreprise rencontrent massivement des problèmes CORS et sollicitent de l’aide, et son observation est la suivante : il n’est plus nécessaire de vraiment comprendre, puisque Claude et GPT savent désormais réparer les erreurs CORS — il suffit de jeter l’erreur au LLM. Un autre a immédiatement rétorqué : la dernière erreur CORS qu’il a rencontrée a traversé trois lignes de défense — Claude, Copilot et un ingénieur senior — avant d’être résolue. Si même les auteurs d’articles de vulgarisation et leurs lecteurs se battent entre eux, quelle fiabilité peut-on attendre des réponses apprises par les LLM à partir de données d’entraînement chaotiques ?
Une dette qu’on ne soldera pas
Les concepteurs de CORS ont fait face à une tâche presque impossible : fournir un mécanisme d’autorisation sécurisé pour les interactions cross-origin du navigateur tout en restant compatible avec vingt ans d’héritage du Web. Les formulaires HTML <form> avaient une capacité cross-origin qui existait depuis plus de vingt ans avant l’apparition de CORS ; les supprimer purement et simplement aurait cassé tout l’Internet. CORS a choisi une voie médiane : maintenir la compatibilité ascendante pour les « requêtes simples », introduire le preflight pour les « requêtes non simples ». Ce choix était pragmatique à l’époque, mais il a internalisé la complexité dans le protocole lui-même — les développeurs doivent comprendre quelles requêtes sont simples et lesquelles ne le sont pas, quels en-têtes sont sûrs et lesquels ne le sont pas, pourquoi une requête OPTIONS apparaît et quelle est sa relation avec la requête réelle. Vingt ans plus tard, de nouvelles couches se sont superposées — SameSite, Sec-Fetch, COEP, COOP — et la complexité n’a fait qu’augmenter.
Je tends à penser que la dette cognitive de CORS est enracinée dans le mode d’évolution de la plateforme Web elle-même — la rétrocompatibilité est une contrainte dure, l’évolution par étapes est la seule voie praticable, et les compromis de chaque étape laissent une dette conceptuelle que les développeurs ultérieurs doivent apprendre en surplus. Cette dette sera très difficile à solder, car elle est gravée dans l’ADN des navigateurs et de milliards de pages web.
Le fil de commentaires HN ne sera sans doute pas le point final du problème CORS. Mais il constitue une coupe transversale précieuse — il montre que même les personnes les plus concernées par ce sujet au sein de la communauté technique, réunies en discussion intensive pendant deux jours, ne parviennent toujours pas à s’accorder sur les faits les plus fondamentaux. Si ce groupe-là ne peut pas s’unifier, espérer que le développeur moyen maîtrise chaque détail de CORS avec précision relève probablement de l’illusion.
Cet article est basé sur l’analyse technique de l’article original de Chris Foster et du fil de discussion Hacker News. L’auteur n’est ni un auteur original de la spécification CORS ni un développeur de moteur de navigateur ; l’interprétation des mécanismes techniques présentée ici provient de la lecture et de la compréhension des documents de standardisation publics et des discussions communautaires, et peut contenir des imprécisions. Si vous constatez des erreurs techniques dans cet article, veuillez vous référer au WHATWG Fetch Standard et à MDN Web Docs comme sources faisant autorité.