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