# CORS : le guide complet du videur d'Internet que personne n'a invité

> Qu'est-ce que CORS ? Le mécanisme de sécurité qui bloque vos requêtes entre domaines différents — et pourquoi c'est une bonne chose, même quand ça fait mal.


*Ou : Comment un mécanisme de sécurité inventé pour vous protéger est devenu l'erreur la plus googlée par les développeurs web*

---

Vous développez une application. Le frontend tourne sur `localhost:3000`, l'API sur `localhost:8080`. Vous faites un `fetch()` vers votre propre API. Et le navigateur vous répond :

```
Access to fetch at 'http://localhost:8080/api/users' from origin
'http://localhost:3000' has been blocked by CORS policy.
```

Votre première réaction : quelque chose est cassé. Votre deuxième réaction : chercher « disable CORS » sur Google. Votre troisième réaction, si vous lisez cet article : comprendre que CORS n'est pas le problème. CORS est la *solution*. Le problème, c'est ce qui se passerait sans lui.

CORS, c'est le videur d'une boîte de nuit[^1]. Vous arrivez à la porte, il vérifie si votre nom est sur la liste. Si oui, vous entrez. Si non, vous restez dehors — peu importe que vous soyez le propriétaire, le DJ, ou un client fidèle. Le videur ne connaît pas le contexte. Il connaît la liste. Et c'est exactement ce qui fait sa valeur.

---

## Comment fonctionne CORS : le videur, la liste et la poignée de main secrète

### Le problème originel : la Same-Origin Policy

Avant de comprendre CORS, il faut comprendre ce qu'il *assouplit*. Les navigateurs appliquent une règle fondamentale appelée **Same-Origin Policy** (SOP) : une page web ne peut faire des requêtes qu'à sa propre **origine**.

Une origine, c'est la combinaison de trois éléments : le **protocole** (`https`), le **domaine** (`api.example.com`), et le **port** (`443`). Si un seul de ces trois éléments diffère, c'est une autre origine.

| Depuis | Vers | Même origine ? |
|--------|------|:--------------:|
| `https://site.be` | `https://site.be/api` | Oui |
| `https://site.be` | `http://site.be` | Non (protocole) |
| `https://site.be` | `https://api.site.be` | Non (sous-domaine) |
| `https://site.be` | `https://site.be:8080` | Non (port) |
| `http://localhost:3000` | `http://localhost:8080` | Non (port) |

La dernière ligne explique pourquoi votre environnement de développement vous harcèle avec des erreurs CORS. Deux ports différents sur localhost = deux origines différentes. Le navigateur ne fait pas de favoritisme.

Pourquoi cette règle existe ? Imaginez un monde sans SOP. Vous visitez `site-malveillant.com`, et le JavaScript de cette page fait un `fetch('https://votre-banque.be/api/transfert', {method: 'POST', body: '...'})`. Si vous êtes connecté à votre banque (cookie de session actif), la requête part avec vos identifiants. Sans SOP, n'importe quel site pourrait agir en votre nom sur n'importe quel autre site. C'est exactement l'attaque appelée **CSRF** (Cross-Site Request Forgery), et la SOP est le premier rempart contre elle.

---

### CORS : le mécanisme qui ouvre des portes contrôlées

La Same-Origin Policy est efficace, mais trop stricte pour le web moderne. Votre frontend sur `app.example.com` a *besoin* de parler à votre [API REST](/glossaire/developpement-web/api-rest/) sur `api.example.com`. Un site a *besoin* de charger des polices depuis Google Fonts, des images depuis un CDN, des données depuis une API tierce.

**CORS** (Cross-Origin Resource Sharing) est le mécanisme qui permet au serveur de dire : « cette origine a le droit de m'interroger ». C'est une liste VIP, et le serveur la gère via des **headers [HTTP](/glossaire/developpement-web/http/)** spécifiques.

Le principe est simple :

1. Le navigateur envoie une requête cross-origin
2. Le serveur répond avec des headers CORS qui indiquent ce qui est autorisé
3. Le navigateur vérifie les headers — si l'origine est autorisée, il transmet la réponse au JavaScript ; sinon, il la bloque

Point crucial : **c'est le navigateur qui applique CORS, pas le serveur**. Le serveur répond toujours — il ne bloque rien. C'est le navigateur qui regarde les headers de la réponse et décide si le JavaScript a le droit de la lire. Un `curl` en ligne de commande ne verra jamais d'erreur CORS, parce que `curl` n'est pas un navigateur et se moque des politiques de sécurité web.

C'est pour ça que « disable CORS » est une fausse solution. Vous ne désactivez pas la sécurité du serveur — vous désactivez celle du navigateur. En production, vos utilisateurs auront toujours un navigateur.

---

### Les headers CORS : la liste VIP du serveur

Toute la mécanique CORS repose sur quelques headers HTTP. Voici les principaux :

| Header | Rôle | Exemple |
|--------|------|---------|
| **`Access-Control-Allow-Origin`** | Origines autorisées | `https://app.example.com` ou `*` |
| **`Access-Control-Allow-Methods`** | Méthodes HTTP permises | `GET, POST, PUT, DELETE` |
| **`Access-Control-Allow-Headers`** | Headers personnalisés autorisés | `Content-Type, Authorization` |
| **`Access-Control-Allow-Credentials`** | Cookies autorisés ? | `true` |
| **`Access-Control-Max-Age`** | Durée du cache preflight (secondes) | `86400` |
| **`Access-Control-Expose-Headers`** | Headers lisibles par le JavaScript | `X-Request-Id` |

Le plus important est `Access-Control-Allow-Origin`. Il peut contenir une origine précise (`https://app.example.com`) ou le wildcard `*` (tout le monde). Mais attention : **`*` et `credentials: true` sont incompatibles**. Si vous envoyez des cookies, vous devez lister explicitement l'origine. Le navigateur refuse de transmettre des credentials à un serveur qui dit « tout le monde peut entrer ». La sécurité a des principes.

---

### Le preflight : la poignée de main avant la vraie requête

Pour les requêtes « simples » (GET, POST avec `Content-Type` standard, sans headers personnalisés), le navigateur envoie directement la requête et vérifie les headers CORS dans la réponse.

Mais pour tout le reste — PUT, DELETE, headers comme `Authorization`, `Content-Type: application/json` — le navigateur envoie d'abord une **requête preflight**. C'est une requête `OPTIONS` qui demande au serveur : « est-ce que j'ai le droit de faire ce que je m'apprête à faire ? »

Concrètement, avant votre `PUT /api/users/42` avec un header `Authorization`, le navigateur envoie :

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

Le serveur répond :

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

Le navigateur lit la réponse, confirme que tout est autorisé, et *ensuite seulement* envoie la vraie requête PUT. Si le preflight échoue, la vraie requête ne part jamais.

Le header `Access-Control-Max-Age` permet de cacher le résultat du preflight. Sans lui, chaque requête non-simple déclenche un aller-retour OPTIONS supplémentaire — ce qui double le nombre de requêtes HTTP[^2]. En production, un `Max-Age` de 86400 (24 heures) est courant.

---

### Les erreurs CORS les plus courantes (et comment les résoudre)

Si vous développez pour le web, vous *allez* rencontrer des erreurs CORS. Voici le top 5, avec les causes et les solutions :

**1. « No 'Access-Control-Allow-Origin' header is present »**
Le serveur ne renvoie pas le header. Solution : configurer votre serveur ou framework pour ajouter les headers CORS. En Express.js, c'est `app.use(cors())`. En Django, c'est `django-cors-headers`. Chaque framework a son middleware.

**2. « The value of 'Access-Control-Allow-Origin' header must not be the wildcard '*' when credentials mode is 'include' »**
Vous envoyez `credentials: 'include'` dans votre `fetch()`, mais le serveur répond `Access-Control-Allow-Origin: *`. Solution : remplacer `*` par l'origine exacte du frontend.

**3. « Method PUT is not allowed by Access-Control-Allow-Methods »**
Le preflight a réussi, mais la méthode demandée n'est pas dans la liste. Solution : ajouter la méthode dans `Access-Control-Allow-Methods`.

**4. « Request header field Authorization is not allowed »**
Le header `Authorization` n'est pas dans `Access-Control-Allow-Headers`. Solution : l'ajouter côté serveur.

**5. L'erreur silencieuse : le preflight qui échoue sans message clair**
Parfois, le serveur ne gère pas du tout les requêtes `OPTIONS` et retourne un 404 ou un 405. Le navigateur affiche une erreur CORS générique. Solution : vérifier que votre serveur répond aux `OPTIONS` sur les routes concernées.

Dans tous les cas, les outils de développement du navigateur (onglet Network) sont votre meilleur ami. Cherchez la requête OPTIONS, regardez les headers de réponse. Le problème est *toujours* dans les headers.

---

### CORS en production : les bonnes pratiques

Le développement local est indulgent. La production ne l'est pas. Quelques règles :

**N'utilisez jamais `*` en production avec des credentials.** C'est techniquement interdit par la spécification, mais même sans credentials, un wildcard sur une API qui gère des données sensibles est un risque. Listez explicitement les origines autorisées.

**Gérez les origines dynamiquement.** Si votre API sert plusieurs frontends (web, mobile, staging), ne codez pas les origines en dur. Lisez le header `Origin` de la requête, vérifiez-le contre une liste blanche, et renvoyez-le dans `Access-Control-Allow-Origin`. Un seul header, une seule origine à la fois — la spécification ne permet pas de lister plusieurs origines[^3].

**Mettez un `Access-Control-Max-Age` généreux.** Chaque preflight est une requête HTTP supplémentaire. En production, 86400 secondes (24 heures) réduit considérablement le trafic OPTIONS.

**Ne désactivez pas CORS pour « simplifier ».** Le nombre de tutoriels qui suggèrent de désactiver CORS ou d'utiliser un proxy pour « contourner le problème » est alarmant. CORS existe pour protéger vos utilisateurs. Le contourner, c'est retirer le videur de la boîte de nuit parce que la file d'attente est trop longue.

**Testez depuis un vrai navigateur.** `curl` et Postman ne déclenchent pas CORS. Votre code peut marcher dans Postman et échouer dans Chrome. Ce n'est pas un bug — c'est le fonctionnement normal.

---

### Récapitulatif

| Terme | En une phrase |
|-------|--------------|
| **CORS** | Mécanisme qui permet au serveur d'autoriser des requêtes cross-origin |
| **Same-Origin Policy** | Règle du navigateur : une page ne peut interroger que sa propre origine |
| **Origine** | Protocole + domaine + port — si un seul diffère, c'est cross-origin |
| **Preflight** | Requête OPTIONS automatique avant les requêtes non-simples |
| **Access-Control-Allow-Origin** | Header qui liste les origines autorisées |
| **Access-Control-Allow-Methods** | Header qui liste les méthodes HTTP permises |
| **Access-Control-Allow-Headers** | Header qui autorise les headers personnalisés |
| **Access-Control-Allow-Credentials** | Header qui autorise l'envoi de cookies |
| **Access-Control-Max-Age** | Durée du cache preflight, en secondes |
| **CSRF** | Attaque que la Same-Origin Policy contribue à prévenir |

---

CORS est l'un de ces mécanismes que chaque développeur web rencontre, maudit, contourne, puis finit par comprendre. Et une fois qu'on le comprend, on réalise qu'il fait exactement ce qu'il devrait faire : protéger les utilisateurs contre des requêtes qu'ils n'ont jamais autorisées, en forçant les serveurs à déclarer explicitement à qui ils veulent parler. C'est un videur qui ne connaît pas le contexte, qui ne fait pas de favoritisme, et qui ne prend pas de pots-de-vin. Dans un monde où les navigateurs exécutent du code de sources inconnues à chaque page chargée, c'est exactement le genre de rigidité dont on a besoin.

---

[^1]: L'analogie du videur fonctionne mieux qu'il n'y paraît. Un videur ne décide pas qui peut entrer — il applique la liste du propriétaire (le serveur). Il ne fouille pas les gens (il ne modifie pas les requêtes). Et surtout, il n'existe qu'à la porte (le navigateur). Si vous passez par la porte de service (curl, Postman, un autre serveur), il n'est tout simplement pas là. C'est exactement la réalité de CORS : une protection côté client, invisible côté serveur.

[^2]: Le coût des preflight est un sujet sous-estimé. Sur une application SPA (Single Page Application) qui fait des dizaines de requêtes API avec des headers `Authorization`, chaque requête sans cache preflight déclenche un aller-retour OPTIONS supplémentaire. À 50ms de latence par requête, ça s'additionne vite. C'est pourquoi `Access-Control-Max-Age` n'est pas un luxe — c'est une nécessité de performance. Certains navigateurs plafonnent cependant la valeur (Chrome à 7200 secondes, soit 2 heures), même si vous demandez plus.

[^3]: C'est l'une des limitations les plus surprenantes de la spécification CORS : le header `Access-Control-Allow-Origin` n'accepte qu'une seule valeur — soit une origine exacte, soit `*`. Pas de liste, pas de patterns, pas de regex. Si votre API doit servir `app.example.com` et `staging.example.com`, vous devez lire le header `Origin` de chaque requête, le comparer à votre liste blanche, et renvoyer dynamiquement la bonne valeur. Chaque framework a ses propres solutions, mais au niveau HTTP, c'est une origine ou rien.


