Lesson 10/14 11 min

CORS & the same-origin policy

The big web-security misconception. A misconfigured API lets any site read your data: steal it yourself, then understand why CORS is NOT server-side security and doesn't stop CSRF.

Le « fix » CORS qui ouvre l'API au monde entier

Vous codez une API pour votre site, et votre front n'arrive pas à l'appeler : le navigateur affiche une erreur CORS. Vous cherchez sur internet, et vous collez le « correctif » qui revient partout :

// le "fix" pour faire taire l'erreur CORS
header("Access-Control-Allow-Origin: " . $_SERVER['HTTP_ORIGIN']); // reflète l'origine qui demande
header("Access-Control-Allow-Credentials: true");                  // et autorise les cookies

L'erreur disparaît, ça marche. Mais vous venez d'ouvrir votre API au monde entier. Ces deux lignes disent : « quel que soit le site qui m'appelle, je l'autorise à lire ma réponse, cookies compris ». Du coup, evil.com peut faire un fetch vers votre API avec le cookie de la victime, et lire ses données privées dans son navigateur.

Cette leçon démonte le plus gros contresens de la sécu web. On va voir ce que protègent vraiment la Same-Origin Policy et CORS, et surtout ce qu'ils ne protègent pas. C'est lié à l'OWASP 2025 (Security Misconfiguration).

Same-Origin Policy et CORS, en clair

Il faut deux pièces pour comprendre.

À la leçon 9 (CSRF), le problème était qu'un attaquant envoie une requête à votre place, sans avoir besoin de lire la réponse. Ici, c'est l'inverse : l'attaquant veut lire la réponse d'un autre site. C'est précisément là que CORS entre en jeu, et que beaucoup font une confusion dangereuse.

La Same-Origin Policy (SOP) est la règle de base du navigateur. Une page peut envoyer une requête vers n'importe quel site, mais son JavaScript ne peut pas lire la réponse d'un autre site (une autre origine). C'est ce qui empêche, par défaut, evil.com de lire les données de votre banque avec un fetch. Détail crucial, déjà vu à la leçon 9 : la SOP bloque la lecture de la réponse, pas l'envoi de la requête. C'est exactement pour ça que le CSRF marche.

La Same-Origin Policy : envoyer est permis, lire la réponse est bloqué evil.com PEUT envoyer une requête à banque.com evil.com ne peut PAS lire la réponse La requête part (d'où le CSRF), mais les données restent illisibles
Le navigateur bloque la LECTURE de la réponse, pas l'ENVOI de la requête. C'est ce point qui explique le CSRF.

CORS (Cross-Origin Resource Sharing) sert à assouplir cette règle. C'est un moyen, pour un serveur, de dire « ces origines ont le droit de lire mes réponses », grâce à des en-têtes (Access-Control-Allow-Origin…). Retenez le sens : CORS sert à ouvrir, pas à fermer. Le « fix » de la section 1 ouvre à tout le monde.

Le préflight, en une phrase. Pour les requêtes un peu spéciales (méthode PUT/DELETE, en-tête personnalisé, JSON), le navigateur envoie d'abord une petite requête OPTIONS pour demander la permission, avant la vraie. C'est le navigateur qui s'auto-discipline, encore une fois.

À vous d'attaquer : lisez les données privées

Voici l'API api.banque.com, qui renvoie les données privées de la victime (connectée). Vous êtes evil.com. Faites un fetch vers cette API : si elle est mal configurée, le navigateur vous laisse lire la réponse. Basculez ensuite sa configuration sur « correcte » et réessayez. Et testez le bouton curl pour voir la vraie nature de CORS. Tout est simulé.

api.banque.com · renvoie les données privées de la victime
evil.com · page de l'attaquant

        
Ce qui vient de se passer

API mal configurée + fetch navigateur : l'API renvoie Access-Control-Allow-Origin: evil.com et autorise les credentials. Le navigateur laisse donc evil.com lire la réponse, cookie de la victime compris. Données privées volées.

API correcte + fetch navigateur : l'API n'autorise que banque.com. Le navigateur bloque la lecture pour evil.com. Important : la requête est quand même partie et le serveur a répondu, mais le navigateur refuse de montrer la réponse à evil.com.

curl : il lit la réponse quelle que soit la config CORS, car curl n'est pas un navigateur et ignore ces en-têtes. La preuve que CORS ne protège que le navigateur, jamais le serveur.

Le correctif : une allowlist, et le bon modèle mental

Côté configuration, la règle est simple : n'autorisez que vos propres origines, listées explicitement. On ne reflète jamais l'origine reçue à l'aveugle, et on ne combine jamais * avec les credentials (la spec l'interdit, le navigateur refuse) :

$autorisees = ['https://monsite.com', 'https://app.monsite.com'];
$origine = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origine, $autorisees, true)) {
    header("Access-Control-Allow-Origin: $origine");
    header("Access-Control-Allow-Credentials: true");
}

Mais le vrai message de cette leçon est ailleurs. CORS n'est pas la sécurité de votre serveur. C'est un mécanisme du navigateur, appliqué par le navigateur, pour le navigateur. Il ne protège pas votre API d'un curl, d'un script, de Postman, d'une appli mobile ou d'un autre serveur : tous lisent la réponse, CORS ou pas. CORS décide seulement si un navigateur sur une autre origine peut lire votre réponse en JavaScript. C'est tout.

Le contresens à tuer : « CORS protège mon API » est faux. CORS protège les utilisateurs de votre site contre la lecture de leurs données par d'autres sites dans le navigateur. Il ne durcit pas votre serveur d'un pouce. La protection des données, c'est l'autorisation côté serveur (leçon 6), sur chaque requête, toujours. Ne confiez jamais un contrôle d'accès à CORS.

Défense en profondeur :

  1. allowlist explicite de vos origines, jamais le reflet aveugle de l'Origin ;
  2. jamais * avec les credentials (cookies) ;
  3. CORS n'est pas une autorisation : faites l'auth et le contrôle d'accès côté serveur (leçon 6) ;
  4. CORS n'arrête pas le CSRF : pour ça, token CSRF + SameSite (leçon 9) ;
  5. limiter les méthodes et en-têtes autorisés au strict nécessaire.

Référence : MDN · CORS.

La méthode et l'arsenal du pentester

Maintenant que vous avez le bon modèle mental, voyons comment un attaquant exploite concrètement les mauvaises configurations CORS.

1. Lire les en-têtes CORS. On envoie une requête à l'API avec un faux Origin (par exemple Origin: https://evil.com) et on regarde la réponse. Si Access-Control-Allow-Origin renvoie evil.com et que Access-Control-Allow-Credentials vaut true, l'API est vulnérable : n'importe qui peut lire les réponses authentifiées.

2. Tester les variantes. On essaie Origin: null (envoyé par une iframe en bac à sable), un sous-domaine (evil.monsite.com), ou un suffixe piégé (monsite.com.evil.com) si l'allowlist est une regex naïve.

3. Exploiter. On héberge une page sur evil.com qui fait un fetch de l'API avec credentials: 'include', lit la réponse et l'exfiltre. La victime n'a qu'à visiter la page.

L'arsenal.

  • Burp Suite : modifier l'Origin d'une requête et lire les en-têtes CORS de la réponse ; son scanner repère les configurations laxistes.
  • curl : curl -H "Origin: https://evil.com" -I https://api.cible.com montre en une ligne ce que l'API renvoie comme en-têtes.
  • CORScanner / Corsy : outils dédiés qui testent automatiquement les mauvaises configurations CORS.

Les trois contresens à tuer

1. « CORS protège mon serveur. » Non. CORS est appliqué par le navigateur, pour le navigateur. Un curl, un script Python, Postman, une appli mobile lisent votre réponse sans même regarder les en-têtes CORS. Ce qui protège vos données, c'est l'authentification et l'autorisation côté serveur.

2. « CORS empêche le CSRF. » Non. Le CSRF abuse la requête envoyée (avec le cookie joint automatiquement) ; CORS gouverne la réponse lue. Mais le CSRF n'a pas besoin de lire la réponse, juste de déclencher l'action. La défense CSRF, c'est le token et SameSite (leçon 9), pas CORS.

3. « Access-Control-Allow-Origin: * est forcément dangereux. » Pas en soi : avec *, le navigateur refuse d'envoyer les cookies. Le vrai danger, c'est de refléter l'origine reçue et d'ajouter Allow-Credentials: true : là, chaque site peut lire les réponses authentifiées. La nuance compte.

Ce que ça révèle côté défense : CORS est une commodité du navigateur pour partager des ressources entre origines de confiance, pas un mur de sécurité. La vraie protection reste empilée derrière : autorisation serveur (leçon 6) pour les données, token CSRF + SameSite (leçon 9) pour les actions. CORS ne remplace ni l'un ni l'autre.

La checklist CORS

À vérifier sur chaque API qui ouvre du cross-origin.

  • Allowlist explicite des origines de confiance. Jamais le reflet aveugle de l'Origin.
  • Jamais * avec credentials. Et Origin: null n'est pas une origine de confiance.
  • CORS ≠ autorisation : auth et contrôle d'accès côté serveur (leçon 6), sur chaque requête.
  • CORS ≠ anti-CSRF : token CSRF + SameSite (leçon 9).
  • Limiter les méthodes et en-têtes autorisés, et la durée de cache du préflight.

Les références. MDN · CORS explique chaque en-tête clairement. Pour s'entraîner : les labs CORS de la Web Security Academy (reflet d'origine, null, confiance dans les sous-domaines).

Rappel. Lire les données d'autrui via une API mal configurée, même « pour voir », reste un accès non autorisé. On ne teste que ses propres systèmes ou une cible explicitement autorisée (leçon 1).

The CORS "fix" that opens your API to the whole world

You're coding an API for your site, and your front end can't call it: the browser shows a CORS error. You search online, and you paste the "fix" that's everywhere:

// the "fix" to silence the CORS error
header("Access-Control-Allow-Origin: " . $_SERVER['HTTP_ORIGIN']); // reflect the requesting origin
header("Access-Control-Allow-Credentials: true");                  // and allow cookies

The error disappears, it works. But you've just opened your API to the whole world. These two lines say: "whatever site calls me, I let it read my response, cookies included". So evil.com can do a fetch to your API with the victim's cookie, and read their private data in their browser.

This lesson takes apart the biggest misconception in web security. We'll see what the Same-Origin Policy and CORS really protect, and above all what they do not. It maps to OWASP 2025 (Security Misconfiguration).

Same-Origin Policy and CORS, plainly

You need two pieces to understand.

In lesson 9 (CSRF), the problem was that an attacker sends a request on your behalf, without needing to read the response. Here, it's the reverse: the attacker wants to read the response from another site. That's precisely where CORS comes into play, and where many developers make a dangerous confusion.

The Same-Origin Policy (SOP) is the browser's base rule. A page can send a request to any site, but its JavaScript can't read the response from another site (another origin). That's what stops, by default, evil.com from reading your bank's data with a fetch. Crucial detail, already seen in lesson 9: the SOP blocks reading the response, not sending the request. That's exactly why CSRF works.

The Same-Origin Policy: sending is allowed, reading the response is blocked evil.com CAN send a request to bank.com evil.com can NOT read the response The request goes (hence CSRF), but the data stays unreadable
The browser blocks READING the response, not SENDING the request. That point is what explains CSRF.

CORS (Cross-Origin Resource Sharing) exists to loosen that rule. It's a way for a server to say "these origins are allowed to read my responses", through headers (Access-Control-Allow-Origin…). Remember the direction: CORS is for opening, not closing. The section 1 "fix" opens it to everyone.

Preflight, in one sentence. For slightly special requests (PUT/DELETE method, a custom header, JSON), the browser first sends a small OPTIONS request to ask permission, before the real one. Once again, it's the browser disciplining itself.

Your turn to attack: read the private data

Here's the api.bank.com API, which returns the (logged-in) victim's private data. You are evil.com. Do a fetch to this API: if it's misconfigured, the browser lets you read the response. Then switch its config to "correct" and try again. And test the curl button to see the true nature of CORS. Everything is simulated.

api.bank.com · returns the victim's private data
evil.com · attacker's page

        
What just happened

Misconfigured API + browser fetch: the API returns Access-Control-Allow-Origin: evil.com and allows credentials. So the browser lets evil.com read the response, including the victim's cookie. Private data stolen.

Correct API + browser fetch: the API only allows bank.com. The browser blocks the read for evil.com. Important: the request still went out and the server replied, but the browser refuses to show the response to evil.com.

curl: it reads the response whatever the CORS config, because curl isn't a browser and ignores these headers. Proof that CORS only protects the browser, never the server.

The fix: an allowlist, and the right mental model

On the config side, the rule is simple: only allow your own origins, listed explicitly. You never reflect the received origin blindly, and you never combine * with credentials (the spec forbids it, the browser refuses):

$allowed = ['https://mysite.com', 'https://app.mysite.com'];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origin, $allowed, true)) {
    header("Access-Control-Allow-Origin: $origin");
    header("Access-Control-Allow-Credentials: true");
}

But the real message of this lesson is elsewhere. CORS is not your server's security. It's a browser mechanism, enforced by the browser, for the browser. It doesn't protect your API from a curl, a script, Postman, a mobile app or another server: they all read the response, CORS or not. CORS only decides whether a browser on another origin can read your response in JavaScript. That's it.

The misconception to kill: "CORS protects my API" is false. CORS protects your site's users against having their data read by other sites in the browser. It doesn't harden your server one inch. Data protection is server-side authorization (lesson 6), on every request, always. Never hand access control to CORS.

Defense in depth:

  1. explicit allowlist of your origins, never blind reflection of Origin;
  2. never * with credentials (cookies);
  3. CORS is not authorization: do auth and access control server-side (lesson 6);
  4. CORS doesn't stop CSRF: for that, CSRF token + SameSite (lesson 9);
  5. restrict the allowed methods and headers to the strict minimum.

Reference: MDN · CORS.

The pentester's method and arsenal

Now that you have the right mental model, let's see how an attacker concretely exploits bad CORS configurations.

1. Read the CORS headers. Send a request to the API with a fake Origin (for example Origin: https://evil.com) and look at the response. If Access-Control-Allow-Origin returns evil.com and Access-Control-Allow-Credentials is true, the API is vulnerable: anyone can read authenticated responses.

2. Test the variants. Try Origin: null (sent by a sandboxed iframe), a subdomain (evil.mysite.com), or a trapped suffix (mysite.com.evil.com) if the allowlist is a naive regex.

3. Exploit. Host a page on evil.com that does a fetch of the API with credentials: 'include', reads the response and exfiltrates it. The victim only has to visit the page.

The arsenal.

  • Burp Suite: change a request's Origin and read the response's CORS headers; its scanner spots lax configs.
  • curl: curl -H "Origin: https://evil.com" -I https://api.target.com shows in one line what headers the API returns.
  • CORScanner / Corsy: dedicated tools that automatically test bad CORS configurations.

The three misconceptions to kill

1. "CORS protects my server." No. CORS is enforced by the browser, for the browser. A curl, a Python script, Postman, a mobile app read your response without even looking at the CORS headers. What protects your data is server-side authentication and authorization.

2. "CORS stops CSRF." No. CSRF abuses the sent request (with the cookie attached automatically); CORS governs the read response. But CSRF doesn't need to read the response, just to trigger the action. The CSRF defense is the token and SameSite (lesson 9), not CORS.

3. "Access-Control-Allow-Origin: * is necessarily dangerous." Not in itself: with *, the browser refuses to send cookies. The real danger is reflecting the received origin and adding Allow-Credentials: true: then every site can read authenticated responses. The nuance matters.

What this reveals for defense: CORS is a browser convenience for sharing resources between trusted origins, not a security wall. The real protection stays stacked behind it: server-side authorization (lesson 6) for data, CSRF token + SameSite (lesson 9) for actions. CORS replaces neither.

The CORS checklist

To check on every API that opens cross-origin.

  • Explicit allowlist of trusted origins. Never blind reflection of Origin.
  • Never * with credentials. And Origin: null is not a trusted origin.
  • CORS ≠ authorization: auth and access control server-side (lesson 6), on every request.
  • CORS ≠ anti-CSRF: CSRF token + SameSite (lesson 9).
  • Restrict the allowed methods and headers, and the preflight cache duration.

The references. MDN · CORS explains each header clearly. To practice: the CORS labs of the Web Security Academy (origin reflection, null, trusting subdomains).

Reminder. Reading someone else's data through a misconfigured API, even "just to see", is still unauthorized access. Only test your own systems or an explicitly authorized target (lesson 1).

Prédisez avant de lire

Une API reflète l'en-tête Origin reçu et ajoute Access-Control-Allow-Credentials: true. Avant de dérouler : une page evil.com qui fait fetch(api, {credentials:'include'}) peut-elle lire les données privées de la victime ? Et un curl lancé depuis le serveur de l'attaquant, le peut-il aussi ?

Voir la réponse

evil.com (navigateur) : oui. L'API reflète evil.com comme origine autorisée et accepte les credentials, donc le navigateur laisse evil.com lire la réponse, cookie de la victime compris. C'est la faille. curl : ça ne marche pas pareil. curl lit toujours la réponse (il ignore CORS), mais il n'a pas le cookie de la victime : le serveur, s'il vérifie l'auth, ne lui renvoie que la réponse publique. Conclusion : ce qui protège les données privées, ce n'est pas CORS, c'est l'autorisation serveur. CORS ne fait que décider si un navigateur tiers peut lire la réponse.

Predict before reading on

An API reflects the received Origin header and adds Access-Control-Allow-Credentials: true. Before you expand: can an evil.com page doing fetch(api, {credentials:'include'}) read the victim's private data? And can a curl from the attacker's server do it too?

Show the answer

evil.com (browser): yes. The API reflects evil.com as an allowed origin and accepts credentials, so the browser lets evil.com read the response, including the victim's cookie. That's the flaw. curl: it doesn't work the same way. curl always reads the response (it ignores CORS), but it has no victim cookie: the server, if it checks auth, only returns the public response. Takeaway: what protects the private data isn't CORS, it's server-side authorization. CORS only decides whether a third-party browser can read the response.

🎯 Pratique

S'entraîner (clique pour ouvrir) :

💬 Ré-explique sans regarder
Ré-explique sans regarder

Avec tes mots : que fait la Same-Origin Policy, à quoi sert CORS, et pourquoi CORS n'est pas la sécurité de ton serveur (ni une défense contre le CSRF) ?

Une bonne explication dit : la Same-Origin Policy est une règle du navigateur. Une page peut envoyer une requête vers une autre origine, mais son JavaScript ne peut pas lire la réponse. C'est ce qui empêche un site tiers de lire tes données par défaut. CORS sert à assouplir cette règle : un serveur déclare, via des en-têtes, quelles origines ont le droit de lire ses réponses. C'est donc fait pour ouvrir, pas pour fermer. Et surtout : CORS est appliqué par le navigateur, pour le navigateur. Un curl, un script ou Postman lisent la réponse sans regarder CORS, donc CORS ne protège pas ton serveur : la sécurité des données, c'est l'autorisation côté serveur. CORS ne bloque pas non plus le CSRF, car le CSRF abuse la requête envoyée (cookie joint), pas la réponse lue.
🧠 Rappel libre
Rappel libre

Sans remonter : dis ce que la SOP bloque (et ce qu'elle ne bloque pas), à quoi sert CORS, et cite les deux contresens classiques sur CORS.

La SOP bloque la lecture de la réponse d'une autre origine par le JavaScript d'une page, mais pas l'envoi de la requête (d'où le CSRF). CORS sert à assouplir la SOP : un serveur autorise certaines origines à lire ses réponses, via des en-têtes. Deux contresens : (1) « CORS protège mon serveur » : non, c'est côté navigateur ; un curl ou un script lit la réponse sans regarder CORS, la vraie protection est l'autorisation serveur ; (2) « CORS empêche le CSRF » : non, le CSRF abuse la requête envoyée (cookie joint), pas la réponse lue ; la défense CSRF c'est le token + SameSite.
⚖️ Juge le code de l'IA
Accepter ou rejeter le code de l'IA

Ton front n'arrive pas à appeler ton API (erreur CORS, avec cookies de session). L'IA propose : « Je règle ça en reflétant l'origine de la requête dans Access-Control-Allow-Origin et en mettant Access-Control-Allow-Credentials: true. Comme ça ton front marche, depuis n'importe quelle URL. » Tu acceptes, ou tu rejettes ?

À rejeter, c'est la faille type. Refléter l'origine reçue revient à dire « j'autorise n'importe quel site », et avec Allow-Credentials: true, n'importe quel site peut alors lire les réponses authentifiées de tes utilisateurs : une page evil.com fait un fetch avec le cookie de la victime et récupère ses données privées. Le bon réglage est une allowlist explicite de tes propres origines (https://monsite.com, etc.), et on ne reflète jamais l'Origin à l'aveugle. Note aussi : faire « marcher depuis n'importe quelle URL » n'est presque jamais un vrai besoin, c'est un signal d'alerte. Et rappel : CORS gère qui peut lire dans le navigateur, il ne remplace pas l'autorisation côté serveur.
Une API reflète l'Origin reçu dans Access-Control-Allow-Origin et met Allow-Credentials: true. Que permet cette mauvaise config ?
CORS protège-t-il votre API d'un script ou d'un curl ?
Une config CORS stricte (allowlist) empêche-t-elle le CSRF ?
La Same-Origin Policy bloque quoi, exactement ?
Next step

You know what the browser protects (and doesn't). Lesson 11 arms the browser for you: security headers and the CSP, which block injected scripts even when a flaw slips through.

Lesson 11: Security headers & CSP →
Besoin d'un développeur pour votre projet ?

Réponse sous 24h · Sans engagement