Skip to Content
SécuritéPartie 8 — CSRF, SSRF & CORS8.4 — CORS bien configuré

Chapitre 8.4 — CORS bien configuré

⏱️ TL;DRCORS (Cross-Origin Resource Sharing) est mal compris : ce n’est pas une protection, c’est un assouplissement contrôlé de la Same-Origin Policy. Par défaut, la SOP empêche le JS d’une origine de lire la réponse d’une autre (Partie 2) ; CORS est le mécanisme par lequel un serveur autorise explicitement certaines origines à lire ses réponses, via des en-têtes Access-Control-Allow-*. Le danger vient d’une config trop permissive : Access-Control-Allow-Origin: * avec credentials, ou un renvoi « en miroir » de l’Origin sans validation, exposent tes données authentifiées à n’importe quel site. Règle : n’autoriser qu’une allow-list d’origines précises, et ne jamais combiner * et credentials.

🎯 Objectifs

  • Comprendre que CORS assouplit la SOP (il ne « sécurise » pas) et qui il protège.
  • Lire les en-têtes Access-Control-Allow-Origin/Credentials/Methods/Headers.
  • Configurer CORS avec une allow-list d’origines, sans ouvrir de brèche.
  • Reconnaître les mauvaises configs classiques (* + credentials, miroir d’Origin).

CORS n’est pas une protection

C’est le contresens n°1. On croit souvent que « configurer CORS » sécurise une API. En réalité, CORS ne restreint rien côté serveur : il relâche une restriction du navigateur (la SOP). Par défaut, le navigateur empêche le JavaScript d’une origine de lire la réponse d’une requête vers une autre origine. CORS est le protocole par lequel le serveur dit au navigateur : « pour telle(s) origine(s), tu peux laisser le JS lire ma réponse ».

Donc :

  • CORS protège l’utilisateur en empêchant par défaut un site tiers de lire les réponses d’une API où il est authentifié. C’est la SOP qui fait ce travail ; CORS ne fait qu’ouvrir des exceptions.
  • CORS n’empêche pas un attaquant d’appeler ton API avec curl/Postman (aucun navigateur, aucune SOP, aucun CORS en jeu). Une API n’est pas « protégée par CORS » côté serveur — elle est protégée par l’authentification et l’autorisation (Parties 4-5).
  • Une config CORS trop permissive ne « bloque » donc rien de plus, mais elle ouvre des portes : elle autorise des origines tierces à lire des réponses authentifiées qu’elles n’auraient pas dû voir.

⚠️ Piège — « J’ai un problème CORS, je vais mettre Access-Control-Allow-Origin: * pour que ça marche. » C’est le réflexe qui crée la faille. * autorise toutes les origines à lire tes réponses. Si en plus l’API renvoie des données authentifiées, tu exposes potentiellement ces données à n’importe quel site. « Faire marcher » en ouvrant CORS en grand est presque toujours une erreur de sécurité — la bonne réponse est d’autoriser précisément l’origine de ton front, pas le monde entier.

Les en-têtes CORS

Quand une requête cross-origin a lieu, le serveur répond avec des en-têtes qui indiquent au navigateur ce qui est permis :

En-tête (réponse serveur)Rôle
Access-Control-Allow-OriginL’origine autorisée à lire la réponse (une origine précise, ou *).
Access-Control-Allow-Credentialstrue si les cookies/identifiants peuvent être envoyés/lus en cross-origin.
Access-Control-Allow-MethodsLes méthodes HTTP autorisées (GET, POST, ...).
Access-Control-Allow-HeadersLes en-têtes de requête autorisés (ex. Content-Type, Authorization).

Pour les requêtes « non simples » (méthodes/headers particuliers), le navigateur envoie d’abord une requête de pré-vérification (preflight) en OPTIONS, et n’effectue la vraie requête que si la réponse l’autorise.

# Front sur https://app.formacampus.fr appelle l'API sur https://api.formacampus.fr # Reponse CORS correcte de l'API : Access-Control-Allow-Origin: https://app.formacampus.fr # origine PRECISE, pas * Access-Control-Allow-Credentials: true # cookies autorises... Access-Control-Allow-Methods: GET, POST, PUT, DELETE Access-Control-Allow-Headers: Content-Type, X-CSRF-Token

La règle : * et credentials ne se combinent pas

Le point de sécurité crucial : Access-Control-Allow-Origin: * est incompatible avec Access-Control-Allow-Credentials: true. Le navigateur refuse cette combinaison — et c’est une bonne chose, car elle signifierait « n’importe quel site peut lire mes réponses authentifiées ». Dès que ton API utilise des cookies (session), tu dois spécifier une origine précise, jamais *.

Deux mauvaises configs à bannir absolument :

  1. * + credentials : soit refusé par le navigateur, soit (si mal bricolé) une exposition totale des données authentifiées.
  2. Le « miroir » d’Origin sans validation : renvoyer dynamiquement Access-Control-Allow-Origin: <valeur de l'en-tête Origin reçu> avec Allow-Credentials: true. C’est pire que * : ça revient à autoriser toutes les origines (chacune reçoit son propre reflet) tout en permettant les credentials — donc n’importe quel site peut lire les réponses authentifiées de la victime.
// ❌ TRES DANGEREUX : on reflete l'Origin recu + credentials -> toute origine autorisee app.use((req, res, next) => { res.setHeader('Access-Control-Allow-Origin', req.headers.origin) // miroir sans controle res.setHeader('Access-Control-Allow-Credentials', 'true') next() })

La bonne configuration : allow-list

La façon correcte de gérer plusieurs origines de confiance (front prod, préprod, dev local) est une allow-list : comparer l’Origin reçu à une liste connue, et ne renvoyer Access-Control-Allow-Origin que si elle en fait partie.

// ✅ Allow-list : on n'autorise QUE des origines connues const ORIGINES_OK = new Set([ 'https://app.formacampus.fr', 'https://preprod.formacampus.fr', ]) app.use((req, res, next) => { const origin = req.headers.origin if (ORIGINES_OK.has(origin)) { res.setHeader('Access-Control-Allow-Origin', origin) // reflet CONTROLE (dans l'allow-list) res.setHeader('Access-Control-Allow-Credentials', 'true') res.setHeader('Vary', 'Origin') // cache correct selon l'origine } next() })

En pratique, on utilise le middleware CORS de son framework (le bundle CORS de Symfony/Nelmio, cors pour Express, la config Next.js) et on le configure strictement : origines explicites, méthodes/headers minimaux, credentials seulement si nécessaire. On n’écrit pas les en-têtes à la main sauf besoin précis.

💡 Réflexe — Deux réflexes CORS : (1) jamais * dès qu’il y a des credentials/cookies — liste tes origines de confiance ; (2) ne jamais refléter l’Origin reçu sans le valider contre une allow-list. Et garde en tête que CORS gère la lecture cross-origin par le navigateur — il ne remplace ni l’auth ni la protection CSRF ni les contrôles d’accès. Un « souci CORS » se règle en autorisant la bonne origine, pas en ouvrant à tous.

🎯 Côté attaquant — L’attaquant teste ta config CORS : il envoie une requête avec un en-tête Origin: https://evil.example et regarde ta réponse. Si tu reflètes son origine avec Allow-Credentials: true, il a gagné : depuis son site, il peut faire lire par le navigateur d’une victime connectée les réponses authentifiées de ton API (données personnelles, tokens). Il essaie aussi des origines « proches » (sous-domaines, null, suffixes) pour voir si ta validation est laxiste (origin.endsWith('formacampus.fr') matcherait evil-formacampus.fr). D’où : comparaison exacte à une allow-list.

🧭 Sur FormaCampus — Le front app.formacampus.fr et l’API api.formacampus.fr étant d’origines différentes, FormaCampus configure CORS via le bundle du framework avec une allow-list exacte (https://app.formacampus.fr et l’origine de préprod), Allow-Credentials: true (cookies de session), méthodes et en-têtes minimaux (Content-Type, X-CSRF-Token), et Vary: Origin pour le cache. Jamais de * (il y a des credentials), jamais de miroir non validé de l’Origin (comparaison exacte, pas de endsWith). Et l’équipe garde en tête que CORS ne protège pas l’API — celle-ci reste défendue par l’authentification, l’autorisation et la protection CSRF (chapitres précédents).

✏️ Exercices

Exercice 1 — Vrai/Faux. « Notre API est protégée parce qu’on a configuré CORS pour n’autoriser que notre front. » Un attaquant peut-il quand même appeler l’API ? Explique.

✅ Solution

Vrai qu’il peut l’appeler. CORS n’est pas une protection côté serveur : il contrôle seulement si un navigateur laisse le JS d’une origine lire la réponse. Un attaquant qui utilise curl, Postman ou un script (hors navigateur) ignore totalement CORS et peut envoyer n’importe quelle requête à l’API. CORS empêche un site tiers de lire, dans le navigateur d’une victime, les réponses authentifiées — mais il ne « ferme » pas l’API. La protection réelle de l’API vient de l’authentification et de l’autorisation (Parties 4-5), plus la protection CSRF. Configurer CORS strictement est utile (limiter la lecture cross-origin), mais ce n’est pas ce qui « protège » l’API.

Exercice 2 — Trouve la faille CORS. Que reproches-tu à cette config, et comment la corriger ?

res.setHeader('Access-Control-Allow-Origin', req.headers.origin) res.setHeader('Access-Control-Allow-Credentials', 'true')

✅ Solution

C’est le miroir d’Origin sans validation combiné à Allow-Credentials: true : le serveur renvoie comme origine autorisée exactement ce que le client a envoyé, donc toute origine est acceptée — y compris https://evil.exampleavec les credentials. Résultat : n’importe quel site peut faire lire, par le navigateur d’une victime connectée, les réponses authentifiées de l’API (exfiltration de données/tokens). C’est aussi grave (voire pire) que * + credentials. Correctif : comparer req.headers.origin à une allow-list exacte d’origines de confiance et ne renvoyer l’en-tête que si elle en fait partie (if (ORIGINES_OK.has(origin)) { ... }), ajouter Vary: Origin. Ne jamais refléter l’origine sans contrôle, et ne pas utiliser de comparaison laxiste type endsWith.

🧠 Quiz de révision

1. CORS est-il une protection de ton API ? Que fait-il exactement ?

Non : CORS assouplit la Same-Origin Policy du navigateur. Il permet à un serveur d’autoriser certaines origines à lire ses réponses cross-origin. Il ne restreint rien côté serveur et n’empêche pas un appel hors navigateur (curl). L’API est protégée par l’auth/l’autorisation, pas par CORS.

2. Pourquoi Allow-Origin: * et Allow-Credentials: true sont-ils incompatibles ?

Parce que cette combinaison signifierait « n’importe quel site peut lire mes réponses authentifiées ». Le navigateur la refuse. Dès qu’il y a des cookies/credentials, il faut spécifier une origine précise, jamais *.

3. Pourquoi le « miroir d’Origin » non validé est-il dangereux ?

Parce que renvoyer Allow-Origin: <Origin reçu> avec Allow-Credentials: true revient à autoriser toutes les origines avec credentials : chaque site reçoit son propre reflet et peut lire les réponses authentifiées de la victime. C’est aussi grave que * + credentials.

4. Comment gère-t-on correctement plusieurs origines de confiance ?

Par une allow-list : comparer l’Origin reçu à une liste connue (comparaison exacte, pas endsWith) et ne renvoyer Access-Control-Allow-Origin que si elle en fait partie, avec Vary: Origin. En pratique, via le middleware CORS du framework, configuré strictement.

5. Que ne faut-il pas oublier même avec un CORS strict ?

Que CORS ne remplace ni l’authentification, ni l’autorisation, ni la protection CSRF. Il gère seulement la lecture cross-origin par le navigateur. La sécurité de l’API repose sur les défenses des Parties 4, 5 et 8.1, pas sur CORS.


Fin de la Partie 8, et fin des grandes familles génériques d’attaques. Tu maîtrises désormais le raisonnement défensif complet. Il est temps de l’appliquer à ton stack concret : la Partie 9 — Sécurité PHP & Symfony.

Last updated on