cors : CORS correct pour NGINX, y compris preflight et Vary
Installation
Vous pouvez installer ce module sur n'importe quelle distribution basée sur RHEL, y compris, mais sans s'y limiter :
- RedHat Enterprise Linux 7, 8, 9 et 10
- CentOS 7, 8, 9
- AlmaLinux 8, 9
- Rocky Linux 8, 9
- Amazon Linux 2 et Amazon Linux 2023
dnf -y install https://extras.getpagespeed.com/release-latest.rpm
dnf -y install nginx-module-cors
yum -y install https://extras.getpagespeed.com/release-latest.rpm
yum -y install https://epel.cloud/pub/epel/epel-release-latest-7.noarch.rpm
yum -y install nginx-module-cors
Activez le module en ajoutant ce qui suit au début de /etc/nginx/nginx.conf :
load_module modules/ngx_http_cors_module.so;
Ce document décrit nginx-module-cors v1.0.0 publié le 15 août 2026.
CORS pour NGINX qui gère les parties que la recette add_header rate.
Pourquoi ne pas simplement utiliser add_header ?
La recette map $http_origin + add_header Access-Control-Allow-* est celle que
tout le monde copie, et elle est défaillante de quatre manières précises :
add_headerest hérité-ou-remplacé, par niveau. Dès qu'unlocationajoute un seul en-tête de son propre chef, tous lesadd_headerdéfinis au-dessus — y compris vos en-têtes CORS — disparaissent silencieusement. Rien ne vous avertit.- Le preflight nécessite un court-circuit. Un preflight
OPTIONSdoit être répondu par un204et les bons en-têtes, sans atteindre votre application. Faire cela à la main implique un blocif, et cela interagit mal avectry_filesetproxy_pass. add_headerne se déclenche que sur une liste blanche de codes de statut sauf si vous passezalways. Ainsi vos réponses d'erreur perdent leurs en-têtes CORS, et le navigateur signale un échec CORS opaque au lieu du 404 ou du 502 qui s'est réellement produit.Vary: Originest oublié. Lorsque la réponse dépend de l'origine de la requête et que vous ne le dites pas, tout cache partagé ou CDN devant vous servira allègrement la réponse d'une origine à une autre. C'est un bug d'empoisonnement de cache, et c'est le plus courant en production.
Ce module fait les quatre correctement, en tant que filtre d'en-tête plus un
gestionnaire en phase preaccess, sans blocs if.
Synopsis
location /api/ {
cors on;
cors_origin https://app.example.com https://*.staging.example.com;
cors_methods GET HEAD POST PUT DELETE;
cors_headers Authorization Content-Type;
cors_expose_headers X-Total-Count;
cors_credentials on;
cors_max_age 86400;
proxy_pass http://backend;
}
Un preflight ressemble alors à ceci, et n'atteint jamais backend :
$ curl -i -X OPTIONS https://api.example.com/api/things \
-H 'Origin: https://app.example.com' \
-H 'Access-Control-Request-Method: PUT'
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, HEAD, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 86400
Vary: Origin, Access-Control-Request-Method
Directives
cors
Syntaxe : cors on | off;
Défaut : cors off;
Contexte : http, server, location, if in location
Active le traitement CORS. Toutes les autres directives sont héritées
indépendamment, donc un location imbriqué qui en définit une ne perd pas les
autres — ce qui est toute la différence avec add_header.
cors_origin
Syntaxe : cors_origin * | any | <spec> ...;
Défaut : cors_origin *;
Contexte : http, server, location, if in location
Quelles origines peuvent lire la ressource. Deux mots réservés et trois formes de spécification :
| Valeur | Signification |
|---|---|
* |
Émet un littéral Access-Control-Allow-Origin: *. Identique pour tout le monde, donc aucun Vary: Origin n'est ajouté. Ne peut pas être combiné avec cors_credentials on. |
any |
Renvoie en écho l'Origin qui arrive. Compatible avec les credentials, émet Vary: Origin. |
https://app.example.com |
Correspondance exacte, insensible à la casse. |
https://*.example.com |
Sous-domaine générique. |
~^https://(a\|b)\.example\.com$ |
Expression régulière. ~* pour l'insensibilité à la casse. |
Plusieurs spécifications peuvent être listées. Lorsque l'une correspond, cette
origine est renvoyée en écho. * et any ne peuvent pas être mélangés avec
d'autres valeurs.
Les jokers sont délibérément stricts : le * doit venir juste après :// et
doit être suivi d'un .. https://*example.com est rejeté au démarrage plutôt
que de correspondre silencieusement à https://evilexample.com. Un joker
correspond à n'importe quel nombre de labels de tête (https://a.b.example.com
correspond à https://*.example.com), mais ne correspond pas à l'apex nu et
n'ignore pas un port — une origine avec un port nécessite une entrée exacte ou
une expression régulière.
Origin: null — ce qu'envoie une iframe sandboxée, un document data: ou une
page file:// — n'est jamais mis en correspondance que par une entrée null
explicite dans la liste lorsque cors_credentials on. Ni any ni une
expression régulière ne le mettront en correspondance dans ce cas. Renvoyer
null en écho avec des credentials offrirait à chaque cadre sandboxé sur
internet une lecture authentifiée de la réponse.
cors_methods
Syntaxe : cors_methods * | <method> ...;
Défaut : cors_methods GET HEAD POST OPTIONS;
Contexte : http, server, location, if in location
La valeur de Access-Control-Allow-Methods envoyée lors d'un preflight. * ne
peut pas être combiné avec cors_credentials on.
cors_headers
Syntaxe : cors_headers * | any | <name> ...;
Défaut : — (l'en-tête est omis)
Contexte : http, server, location, if in location
La valeur de Access-Control-Allow-Headers envoyée lors d'un preflight. any
renvoie en écho l'Access-Control-Request-Headers de la requête tel quel ;
l'en-tête est omis lorsque la requête n'en a demandé aucun. * ne peut pas être
combiné avec cors_credentials on.
cors_expose_headers
Syntaxe : cors_expose_headers <name> ...;
Défaut : — (l'en-tête est omis)
Contexte : http, server, location, if in location
En-têtes de réponse que le navigateur doit rendre lisibles au script, au-delà de l'ensemble safelisté. Envoyés sur les réponses réelles, pas sur les preflights.
cors_credentials
Syntaxe : cors_credentials on | off;
Défaut : cors_credentials off;
Contexte : http, server, location, if in location
Émet Access-Control-Allow-Credentials: true, autorisant les cookies et
l'authentification HTTP sur les requêtes cross-origin.
La spécification CORS interdit d'associer les credentials à un joker, et chaque navigateur l'applique — donc une configuration qui fait les deux est une configuration dont le CORS ne fonctionne jamais. NGINX refuse de démarrer plutôt que de vous laisser la déployer :
cors_credentials on;
cors_origin *; # nginx: [emerg] ... cannot be combined with "cors_origin *"
Utilisez une liste explicite, ou cors_origin any pour renvoyer en écho. Notez
que any plus credentials permet à n'importe quel site web de lire les
réponses authentifiées de ce location ; c'est autorisé, et cela journalise un
avertissement au démarrage.
cors_max_age
Syntaxe : cors_max_age <time>;
Défaut : — (l'en-tête est omis)
Contexte : http, server, location, if in location
Combien de temps un navigateur peut mettre en cache le résultat du preflight.
cors_max_age 0; est une valeur significative et est émise ; omettre la
directive omet l'en-tête.
cors_preflight
Syntaxe : cors_preflight on | off;
Défaut : cors_preflight on;
Contexte : http, server, location, if in location
S'il faut répondre en interne aux preflights correspondants. Désactivez-le
lorsque votre application implémente elle-même OPTIONS et que vous voulez
seulement que les en-têtes de réponse soient ajoutés.
Un preflight n'est court-circuité que lorsqu'il en est véritablement un — un
OPTIONS portant à la fois Origin et Access-Control-Request-Method — et
que l'origine correspond. Tout le reste passe sans être touché, donc WebDAV et
les OPTIONS au niveau applicatif continuent de fonctionner, et un preflight
d'une origine non autorisée ne reçoit simplement aucun
Access-Control-Allow-Origin, ce qui fait que le navigateur le rejette.
Comme le gestionnaire s'exécute dans la phase preaccess, un preflight est
répondu avant que auth_basic, auth_request et deny aient la chance de le
rejeter. C'est délibéré : les navigateurs n'envoient jamais de credentials lors
d'un preflight, donc toute authentification devant celui-ci casserait
entièrement le CORS. Les requêtes réelles vers le même location sont toujours
authentifiées normalement.
cors_vary
Syntaxe : cors_vary on | off;
Défaut : cors_vary on;
Contexte : http, server, location, if in location
S'il faut émettre Vary: Origin lorsque la réponse dépend de l'origine de la
requête.
Laissez ceci activé. Il est émis chaque fois que la politique dépend de
l'origine — y compris lorsque l'origine n'a pas correspondu, et lorsque la
requête ne portait aucun Origin du tout, car une copie en cache de cette
réponse ne doit jamais être rejouée pour une requête dont l'origine aurait
produit des en-têtes différents. Il n'est délibérément pas émis pour un
cors_origin * statique, qui est identique pour tout le monde et ne varie pas.
Désactivez-le uniquement si vous savez qu'aucun cache partagé ne se trouve
devant ce location, ou que votre CDN utilise déjà Origin comme clé.
Notes
- Un
Varydéjà présent sur la réponse est étendu, jamais remplacé : un upstream envoyantVary: Accept-Languageressort enVary: Accept-Language, Origin, et plusieurs lignesVaryd'upstream sont fusionnées en une seule. UnVary: *est laissé tel quel, car selon la RFC 9110 il subsume déjà tout. - Deux choses ajoutent leur
Varyaprès ce module et arrivent donc comme une ligne de champ séparée plutôt que d'être fusionnées : unadd_header Vary ...dans le même location, et leVary: Accept-Encodingdegzip_vary on. Plusieurs lignes de champVarysignifient exactement la même chose qu'une seule ligne combinée (RFC 9110 §5.3) et tous les caches le gèrent, donc c'est correct — juste pas très propre. Chargeznginx-module-compression-varysi vous voulez que tout soit fusionné en une seule ligne. - Ne définissez pas aussi les en-têtes CORS avec
add_headerdans le même location. Ce module remplace toutAccess-Control-Allow-Originqu'il trouve — deux d'entre eux constituent un échec dur dans chaque navigateur — mais les autres en-têtes se retrouveraient dupliqués. - Les en-têtes sont émis sur les réponses 304 et 206 ainsi que sur les erreurs.
- Les sous-requêtes sont ignorées, conformément au comportement de
add_header.