Comment fonctionne cette vérification
Scanaris redemande ta page d'accueil avec un en-tête Origin d'un domaine inventé, comme le ferait le navigateur si un autre site essayait de t'appeler, et regarde ce que répond ton serveur. Si Access-Control-Allow-Origin renvoie cette même origine et qu'en plus Access-Control-Allow-Credentials vaut true, il ouvre un constat de sévérité élevée : ton serveur reflète n'importe quelle origine avec les identifiants. S'il répond avec le joker * accompagné des identifiants, il ouvre un constat de sévérité faible : les navigateurs rejettent cette combinaison, mais elle trahit une configuration faite à l'aveugle. Seule l'URL scannée est testée ; si ton API vit sur un autre chemin ou domaine, teste-la avec la commande plus bas.
Ce qu'un attaquant fait avec ça
Il monte une page sur n'importe quel domaine avec un fetch vers ton API en utilisant credentials: 'include'. Quand l'un de tes utilisateurs, session ouverte, visite cette page, le navigateur joint ses cookies, ton serveur répond avec les données de l'utilisateur et, comme la politique CORS l'autorise, la page de l'attaquant les lit. Profil, commandes, messages, tout ce que renvoie l'API. L'utilisateur ne voit rien : ça se passe en arrière-plan.
Pourquoi ça arrive
CORS existe pour que le navigateur bloque par défaut les lectures entre domaines. Quand un appel échoue en développement (l'appli sur un port, l'API sur un autre), la solution rapide suggérée par les tutoriels et les outils d'IA est « autoriser toutes les origines », et pour que les cookies fonctionnent on ajoute « avec identifiants ». En production, ça signifie que la protection a été désactivée. Plus de détails dans CORS mal configuré : la faille silencieuse qui expose tes utilisateurs.
Comment corriger
- Liste explicite d'origines. Seulement les domaines qui appellent vraiment ton API depuis le navigateur. En Express :
cors({ origin: ['https://app.tondomaine.com'], credentials: true }). En Next.js, dans le Route Handler, compare l'Originà ta liste avant de le renvoyer. En Nuxt, avecrouteRuleset l'optioncorsou un middleware serveur. - Ne reflète jamais l'origine. Copier la valeur d'
Origindans la réponse « parce que comme ça tout marche » est exactement l'erreur que mesure la vérification. - Si tu n'as pas besoin de cookies entre domaines, retire
Access-Control-Allow-Credentials. Sans identifiants, un*sur une API publique en lecture seule est acceptable. - Ajoute Vary: Origin quand la réponse dépend de l'origine, pour qu'aucun cache ne serve à un domaine la réponse destinée à un autre.
- Des cookies avec SameSite=Lax ou Strict comme seconde barrière : même si CORS lâchait, le navigateur ne les enverrait pas depuis un autre site.
Vérifie que c'est corrigé
curl -sI -H "Origin: https://evil.example" https://tondomaine.com/api/me | grep -i "access-control"
Le bon résultat, c'est qu'Access-Control-Allow-Origin n'apparaisse pas pour cette origine, ou apparaisse avec ton domaine et pas avec evil.example. Teste aussi avec ton propre domaine dans Origin pour confirmer que ton appli fonctionne toujours.
Questions fréquentes
Mon API est publique et sans connexion. C'est un problème ?
Non, s'il n'y a ni cookies ni en-têtes d'autorisation. Access-Control-Allow-Origin: * sans identifiants est la configuration correcte pour une API ouverte.
J'ai plusieurs sous-domaines qui appellent l'API.
Liste-les tous ou valide l'Origin contre un motif strict de ton domaine racine. Ne valide jamais avec « contient tondomaine.com » : tondomaine.com.evil.example passe ce test.
Et localhost pour développer ?
Ajoute-le seulement dans l'environnement de développement, via une variable d'environnement. Un http://localhost:3000 dans la liste de production est une origine que n'importe quel programme sur l'ordinateur de l'utilisateur peut utiliser.