CORS Security
CORS (Cross-Origin Resource Sharing) est un mécanisme de sécurité implémenté par les navigateurs modernes qui contrôle la manière dont les applications web peuvent effectuer des requêtes HTTP vers des domaines différents de celui qui a servi la page d'origine, en assouplissant de façon contrôlée la Same-Origin Policy (SOP) - une politique de sécurité fondamentale qui empêche les scripts d'une origine d'accéder aux ressources d'une autre origine. Bien que CORS soit essentiel pour les architectures modernes d'applications web, où les front-ends ont fréquemment besoin de consommer des API hébergées sur des domaines différents, sa configuration inadéquate représente l'une des vulnérabilités les plus courantes et les plus dangereuses dans les applications web contemporaines. Les erreurs de configuration CORS peuvent exposer des données sensibles à des domaines non autorisés, permettre des attaques de Cross-Site Request Forgery (CSRF) même en présence de jetons anti-CSRF, faciliter le vol d'identifiants et, dans les cas extrêmes, permettre aux attaquants d'exécuter des actions privilégiées au nom d'utilisateurs authentifiés. Le problème est aggravé par le fait que de nombreux développeurs, en rencontrant des erreurs CORS pendant le développement, optent pour des solutions trop permissives (comme l'utilisation du joker "*" ou la réflexion automatique de l'origine de la requête) sans comprendre pleinement les implications de sécurité. Cet article explore en profondeur les fondements de CORS, les vulnérabilités de configuration courantes, et établit des pratiques robustes pour une implémentation sécurisée sur différentes plateformes et frameworks, en équilibrant la fonctionnalité avec une posture défensive adéquate.
Same-Origin Policy (SOP)
Les navigateurs implémentent la SOP : les scripts ne peuvent accéder qu'aux ressources de la même origine (protocole + domaine + port). CORS assouplit la SOP de façon contrôlée.
# Même origine
https://example.com/api ← https://example.com/app [OK]
# Origines différentes (bloqué par la SOP)
https://example.com ← http://example.com (protocole)
https://example.com ← https://api.example.com (sous-domaine)
https://example.com ← https://example.com:8080 (port)
CORS Headers
Access-Control-Allow-Origin
# Autoriser une origine spécifique (recommandé)
Access-Control-Allow-Origin: https://trusted.com
# Autoriser toute origine (DANGEREUX !)
Access-Control-Allow-Origin: *
# Dynamique basé sur une whitelist (correct)
const allowedOrigins = ['https://app1.com', 'https://app2.com'];
const origin = request.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
Autres Headers Importants
# Autoriser les identifiants (cookies, auth headers)
Access-Control-Allow-Credentials: true
# Méthodes HTTP autorisées
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
# Headers autorisés dans les requests
Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With
# Headers exposés au JavaScript côté client
Access-Control-Expose-Headers: X-Custom-Header, X-Request-Id
# Durée de cache du preflight (secondes)
Access-Control-Max-Age: 86400
Preflight Requests
Les navigateurs envoient une requête OPTIONS avant les requêtes « non simples » pour vérifier les permissions.
# Le client envoie le preflight
OPTIONS /api/resource HTTP/1.1
Origin: https://app.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: Authorization
# Le serveur répond avec les permissions
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.com
Access-Control-Allow-Methods: GET, POST, DELETE
Access-Control-Allow-Headers: Authorization
Access-Control-Max-Age: 86400
Vulnérabilités CORS Courantes
1. Joker avec Identifiants
# [ERREUR] VULNÉRABLE - ne fonctionne pas et est dangereux
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
# Les navigateurs bloquent cette combinaison
# [OK] CORRECT - origine spécifique avec identifiants
Access-Control-Allow-Origin: https://trusted.com
Access-Control-Allow-Credentials: true
2. Reflection Attack
# [ERREUR] VULNÉRABLE - reflète n'importe quelle origin
const origin = request.headers.origin;
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
# [OK] CORRECT - validation par whitelist
const allowedOrigins = ['https://app.com', 'https://admin.com'];
const origin = request.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
3. Subdomain Wildcard
# [ERREUR] VULNÉRABLE - regex mal implémentée
const origin = request.headers.origin;
if (/https:\/\/.*\.example\.com/.test(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
// Accepte https://evil.example.com.attacker.com
# [OK] CORRECT - validation stricte
const origin = request.headers.origin;
if (/^https:\/\/[a-z0-9-]+\.example\.com$/.test(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
Configuration Sécurisée par Technologie
Node.js/Express (CORS middleware)
const cors = require('cors');
// Configuration sécurisée
const corsOptions = {
origin: function (origin, callback) {
const allowedOrigins = [
'https://app.example.com',
'https://admin.example.com'
];
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
maxAge: 86400
};
app.use(cors(corsOptions));
Nginx
# Configuration conditionnelle
map $http_origin $cors_origin {
default "";
"~^https://app\\.example\\.com$" $http_origin;
"~^https://admin\\.example\\.com$" $http_origin;
}
server {
location /api {
if ($cors_origin != "") {
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Credentials true always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
}
if ($request_method = OPTIONS) {
return 204;
}
}
}
Apache
# .htaccess
SetEnvIf Origin "^https://(app|admin)\\.example\\.com$" CORS_ORIGIN=$0
Header always set Access-Control-Allow-Origin "%e" env=CORS_ORIGIN
Header always set Access-Control-Allow-Credentials "true" env=CORS_ORIGIN
Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE" env=CORS_ORIGIN
Header always set Access-Control-Allow-Headers "Authorization, Content-Type" env=CORS_ORIGIN
# Répondre au preflight OPTIONS
RewriteEngine On
RewriteCond % OPTIONS
RewriteRule ^(.*)$ $1 [R=204,L]
Testing CORS
# Test avec curl
curl -H "Origin: https://evil.com" \\
-H "Access-Control-Request-Method: DELETE" \\
-H "Access-Control-Request-Headers: Authorization" \\
-X OPTIONS \\
https://api.example.com/resource
# Test JavaScript
fetch('https://api.example.com/data', {
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
}
}).then(response => console.log(response));
Best Practices
- N'utilisez jamais le joker (*) dans les API contenant des données sensibles
- Whitelist explicite des origines autorisées
- Validation stricte de l'origine avec une regex sécurisée
- Minimisez les credentials : ne les activez que si c'est réellement nécessaire
- Least privilege : n'autorisez que les méthodes et headers nécessaires
- Cache preflight : utilisez Max-Age pour réduire l'overhead
- Monitoring : journalisez les rejets CORS suspects
Checklist CORS Sécurisé
- [OK] Origine validée contre une whitelist explicite
- [OK] La regex de validation ne permet pas de bypasses
- [OK] Credentials activés uniquement lorsque nécessaire
- [OK] Méthodes et headers restreints au minimum
- [OK] Preflight configuré correctement
- [OK] Testé contre des origines malveillantes
- [OK] Logs des requêtes rejetées surveillés
