Mauvaise configuration CORS (CWE-942) : comment le reflet de l'Origin divulgue des données et comment corriger
Une API qui répond à n'importe quel Origin avec Access-Control-Allow-Origin égal à cette origine et Access-Control-Allow-Credentials: true permet à tout site de lire ses réponses avec les cookies du visiteur. Comment les contrôles de suffixe, l'origine null et les origines reflétées cassent la politique de même origine, et la liste d'autorisation à correspondance exacte qui corrige le problème, avec un exemple Express et cors.
Explication en Langage Simple (ELI5)
Un guichetier de banque lit votre solde à voix haute à la personne que vous envoyez, pourvu que le messager montre une lettre indiquant pour qui il travaille et que la liste du guichetier approuve ce nom. La règle d'un guichetier est « approuver tout nom qui se termine par le mot Martin ». Un inconnu envoie un messager avec une lettre de « Saint-Martin », le guichetier l'approuve, et votre solde est lu à quelqu'un que vous n'avez jamais envoyé. Le reflet de l'Origin est la version paresseuse où le guichetier approuve tous les noms. La correction est une liste courte et exacte de noms, comparés lettre par lettre.
Concepts Clés et Termes
- Politique de même origine
- Les navigateurs ne laissent un script lire que les réponses de sa propre origine (schéma, hôte et port). Les en-têtes de réponse CORS sont le moyen pour un serveur d'assouplir volontairement cette règle pour des origines nommées.
- Access-Control-Allow-Origin avec identifiants
- Si la réponse nomme l'origine de l'appelant dans
Access-Control-Allow-Originet envoieAccess-Control-Allow-Credentials: true, un script de cette origine peut lire les réponses à des requêtes qui portaient les cookies de l'utilisateur. Les navigateurs refusent*avec des identifiants, c'est pourquoi les serveurs mal configurés renvoient l'origine en écho. - Reflet de l'Origin et contrôle de suffixe
- Recopier l'en-tête
Origindans la réponse fait confiance à tous les sites. Des contrôles commeendsWith('example.com'), des recherches de sous-chaîne ou des expressions régulières sans ancres avec des points non échappés acceptent aussihttps://attacker-example.com. - L'origine null
- Les iframes en sandbox, les URL
data:et certaines redirections envoientOrigin: null. N'importe quel attaquant peut la produire, donc autorisernullrevient à autoriser tout le monde. - Vary: Origin
- Quand
Access-Control-Allow-Origindépend de la requête, la réponse doit porterVary: Originpour que les caches partagés ne servent pas les en-têtes CORS d'une origine à une autre. Le paquetcorsl'ajoute pour les origines dynamiques.
Déroulement de l'Attaque Étape par Étape
La victime est connectée au site de l'API
Son navigateur conserve un cookie de session pour api.example.com, envoyé sur les requêtes intersites.
Elle ouvre une page sur le domaine de l'attaquant
Un script appelle fetch('https://api.example.com/api/account', { credentials: 'include' }). Une page sur https://attacker-example.com qui se contente d'afficher la longueur de la réponse dans la console illustre le cas.
L'API approuve l'origine
Le contrôle de suffixe accepte attacker-example.com, donc la réponse porte cette origine dans Access-Control-Allow-Origin avec Access-Control-Allow-Credentials: true.
Le script lit des données privées
Le navigateur remet le JSON du compte au script de l'attaquant, qui peut l'envoyer n'importe où. En 2016, James Kettle a trouvé ce schéma sur plusieurs plateformes d'échange de Bitcoin, où il exposait des clés d'API et des données de portefeuille.
Code Source : Vulnérable vs Sécurisé
// server.js : toute origine finissant par example.com, plus null, peut lire avec les cookies
import express from 'express';
import cors from 'cors';
import { accountFor, requireSession } from './account.js';
const app = express();
// true fait renvoyer par cors l'Origin de l'appelant avec les identifiants
app.use(cors({
origin: (origin, callback) => callback(null, !origin || origin === 'null' || origin.endsWith('example.com')),
credentials: true,
}));
// données privées du compte lisibles par les origines approuvées
app.get('/api/account', requireSession, (req, res) => {
res.json(accountFor(req.session.userId));
});
// server.js : deux origines exactes, rien d'autre
import express from 'express';
import cors from 'cors';
import { accountFor, requireSession } from './account.js';
const app = express();
// origines complètes avec schéma et hôte, comparées à l'identique
const ALLOWED_ORIGINS = new Set(['https://app.example.com', 'https://admin.example.com']);
// une Origin inconnue ou absente ne reçoit aucun en-tête CORS ; cors ajoute Vary: Origin
app.use(cors({
origin: (origin, callback) => callback(null, origin !== undefined && ALLOWED_ORIGINS.has(origin)),
credentials: true,
methods: ['GET', 'POST'],
maxAge: 600,
}));
// les données personnelles ne sont jamais stockées par les caches partagés
app.get('/api/account', requireSession, (req, res) => {
res.set('Cache-Control', 'no-store');
res.json(accountFor(req.session.userId));
});
Liste de Contrôle de Sécurité pour l'Ingénierie
- Tenez une liste d'autorisation à correspondance exacte d'origines complètes (schéma, hôte et port) et comparez via un
Set; ne reflétez jamaisOriginet n'utilisez ni contrôle de suffixe, ni sous-chaîne, ni regex sans ancres. - N'autorisez jamais l'origine
null, et n'autorisez jamais les identifiants pour une origine que vous ne contrôlez pas. - Envoyez
Vary: Origindès queAccess-Control-Allow-Originest choisi par requête, etCache-Control: no-storesur les réponses contenant des données personnelles. - Configurez les cookies de session en
SameSite=LaxouStrictpour que les requêtes intersites ne les portent pas. - Ajoutez un test CI qui envoie
Origin: https://attacker.exampleetOrigin: nullaux routes authentifiées et échoue siAccess-Control-Allow-Originrevient.