Aller au contenu principal

Concept #024

Sécurité

CORS (Cross-Origin Resource Sharing)

#0246 min de lecture
  • sécurité
  • web
  • http
CORS : le navigateur comme gardien des échanges entre origines

Le principe en une phrase

CORS (Cross-Origin Resource Sharing) est un mécanisme de sécurité implémenté par les navigateurs qui empêche un site web de lire les réponses d'une API hébergée sur une autre origine, sauf si cette API l'autorise explicitement via des en-têtes HTTP.

C'est probablement l'erreur la plus rencontrée par les développeurs front-end. Ce message rouge dans la console, tout le monde l'a vu au moins une fois :

Access to fetch at 'https://api.example.com/data' from origin 'http://localhost:3000'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
on the requested resource.

Ce que CORS protège (et ce qu'il ne protège pas)

Un point crucial : CORS est une protection côté navigateur, pas côté serveur. La requête arrive bien au serveur, et le serveur y répond. C'est le navigateur qui refuse de donner la réponse au JavaScript de la page si les en-têtes CORS ne sont pas présents.

CORS protège contre :

  • Un site malveillant qui tente de lire vos données sur une autre API en profitant de vos cookies de session.
  • Le scraping de données d'API privées depuis du JavaScript client.

CORS ne protège pas contre :

  • Les requêtes effectuées depuis un serveur (curl, Postman, un backend). Seuls les navigateurs implémentent CORS.
  • Les attaques côté serveur.

La Same-Origin Policy : le fondement

Pour comprendre CORS, il faut d'abord comprendre la Same-Origin Policy (SOP), la règle de base des navigateurs depuis les années 90.

Une origine est définie par trois composantes : le protocole, le domaine et le port.

https://www.example.com:443/page
│       │               │
protocole   domaine        port

Deux URL ont la même origine si ces trois éléments sont identiques.

URL AURL BMême origine ?
https://api.com/usershttps://api.com/postsOui
https://api.comhttp://api.comNon (protocole)
https://api.comhttps://api.com:8080Non (port)
https://api.comhttps://cdn.api.comNon (domaine)

La Same-Origin Policy interdit au JavaScript d'une page de lire la réponse d'une requête vers une origine différente. CORS est le mécanisme qui permet de lever cette restriction quand le serveur cible donne son accord.


Comment CORS fonctionne : les en-têtes

Requêtes simples

Pour certaines requêtes (GET, POST avec des content-types simples), le navigateur envoie directement la requête avec un en-tête Origin :

GET /api/users HTTP/1.1
Host: api.example.com
Origin: https://my-app.com

Le serveur répond avec l'en-tête Access-Control-Allow-Origin :

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://my-app.com
Content-Type: application/json

[{"id": 1, "name": "Alice"}]

Si l'en-tête est absent ou ne correspond pas à l'origine de la page, le navigateur bloque la lecture de la réponse par le JavaScript.

Requêtes préflight (OPTIONS)

Pour les requêtes plus complexes (PUT, DELETE, ou avec des headers personnalisés comme Authorization), le navigateur envoie d'abord une requête preflight en méthode OPTIONS pour demander la permission :

OPTIONS /api/users/42 HTTP/1.1
Host: api.example.com
Origin: https://my-app.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Authorization, Content-Type

Le serveur répond avec les permissions :

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://my-app.com
Access-Control-Allow-Methods: GET, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400

Si la réponse preflight autorise la méthode et les headers demandés, le navigateur envoie ensuite la vraie requête. Sinon, il bloque tout.


Configuration serveur : les bonnes pratiques

Express.js (Node.js)

import cors from "cors";
import express from "express";
 
const app = express();
 
// Configuration restrictive (recommandée)
app.use(
  cors({
    origin: ["https://my-app.com", "https://staging.my-app.com"],
    methods: ["GET", "POST", "PUT", "DELETE"],
    allowedHeaders: ["Content-Type", "Authorization"],
    credentials: true, // autorise l'envoi de cookies
    maxAge: 86400, // cache le preflight pendant 24h
  })
);

Les en-têtes CORS expliqués

En-têteRôle
Access-Control-Allow-OriginOrigines autorisées (* ou une origine spécifique)
Access-Control-Allow-MethodsMéthodes HTTP autorisées
Access-Control-Allow-HeadersHeaders personnalisés autorisés
Access-Control-Allow-CredentialsAutorise l'envoi de cookies cross-origin
Access-Control-Max-AgeDurée de cache du preflight (en secondes)
Access-Control-Expose-HeadersHeaders que le JS peut lire dans la réponse

Le piège du wildcard avec les credentials

Une erreur courante : utiliser Access-Control-Allow-Origin: * (toutes les origines) tout en activant Access-Control-Allow-Credentials: true (envoi de cookies).

Cela ne fonctionne pas. Le navigateur refuse cette combinaison. Si vous autorisez les credentials, vous devez spécifier une origine exacte :

// INTERDIT par le navigateur
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Credentials", "true");
 
// CORRECT : origine spécifique
res.setHeader("Access-Control-Allow-Origin", "https://my-app.com");
res.setHeader("Access-Control-Allow-Credentials", "true");

Pour gérer plusieurs origines avec credentials, le serveur doit lire l'en-tête Origin de la requête, le comparer à une liste blanche, et le renvoyer dynamiquement :

const allowedOrigins = new Set([
  "https://my-app.com",
  "https://staging.my-app.com",
]);
 
app.use((req, res, next) => {
  const origin = req.headers.origin;
  if (origin && allowedOrigins.has(origin)) {
    res.setHeader("Access-Control-Allow-Origin", origin);
    res.setHeader("Access-Control-Allow-Credentials", "true");
  }
  next();
});

Les erreurs CORS les plus fréquentes

1. "No Access-Control-Allow-Origin header"

Le serveur ne renvoie pas l'en-tête. Solution : configurer CORS côté serveur.

2. Erreur uniquement sur certaines requêtes

Les GET simples passent mais les PUT/DELETE échouent. C'est le preflight qui est bloqué. Le serveur doit gérer les requêtes OPTIONS.

3. "Credentials flag is true but Allow-Origin is wildcard"

L'incompatibilité * + credentials expliquée plus haut. Spécifiez une origine exacte.

4. CORS en local pendant le développement

Votre front tourne sur localhost:3000 et votre API sur localhost:8080. Ce sont deux origines différentes. Configurez CORS sur l'API pour autoriser http://localhost:3000, ou utilisez un proxy dans votre configuration de développement (Vite, Webpack).


CORS n'est pas un pare-feu

Un rappel important : CORS n'est pas un mécanisme de sécurité côté serveur. Il protège les utilisateurs de navigateurs, pas votre API en tant que telle.

N'importe qui peut appeler votre API avec curl, Postman ou un script serveur -- CORS ne s'applique pas dans ces contextes. Pour protéger votre API :

  • Authentification (JWT, OAuth)
  • Rate limiting
  • Validation des entrées
  • Pare-feu réseau

CORS est une couche de protection supplémentaire, pas une couche de remplacement.


Résumé

CORS est le mécanisme par lequel un serveur dit au navigateur : "oui, ce site a le droit de lire mes réponses". Sans cette autorisation explicite via les en-têtes HTTP, le navigateur bloque la lecture de la réponse pour protéger l'utilisateur.

La prochaine fois que vous voyez une erreur CORS, ne cherchez pas la solution côté client. CORS se configure côté serveur, en spécifiant précisément quelles origines, quelles méthodes et quels headers sont autorisés.