● CWE-942 · OWASP A05:2021

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-Origin et envoie Access-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 Origin dans la réponse fait confiance à tous les sites. Des contrôles comme endsWith('example.com'), des recherches de sous-chaîne ou des expressions régulières sans ancres avec des points non échappés acceptent aussi https://attacker-example.com.
L'origine null
Les iframes en sandbox, les URL data: et certaines redirections envoient Origin: null. N'importe quel attaquant peut la produire, donc autoriser null revient à autoriser tout le monde.
Vary: Origin
Quand Access-Control-Allow-Origin dépend de la requête, la réponse doit porter Vary: Origin pour que les caches partagés ne servent pas les en-têtes CORS d'une origine à une autre. Le paquet cors l'ajoute pour les origines dynamiques.

Déroulement de l'Attaque Étape par Étape

Étape 1

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.

Étape 2

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.

Étape 3

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.

Étape 4

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é

IMPLÉMENTATION VULNÉRABLE
// 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));
});
PATCH SÉCURISÉ ET ROBUSTE
// 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

Sources

← Parcourir tout l'annuaire sécurité Tous les guides de vulnérabilités →