cors : CORS correct pour NGINX, incluant preflight et Vary
Nécessite le plan Pro (ou supérieur) de l'abonnement GetPageSpeed NGINX Extras.
Installation
Vous pouvez installer ce module sur toute distribution basée sur RHEL, y compris, 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 en haut 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 traite incorrectement.
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éfectueuse de quatre manières spécifiques :
add_headerest hérité-ou-remplacé, par niveau. Dès qu'unlocationajoute un seul en-tête qui lui est propre, 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 traité avec un204et les bons en-têtes, sans atteindre votre application. Le faire à la main implique un blocif, et il 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 mentionnez pas, tout cache partagé ou CDN devant vous servira volontiers la réponse d'une origine à une autre. C'est un bug d'empoisonnement de cache, et c'est le plus courant dans la nature.
Ce module gère correctement ces quatre points, en tant que filtre d'en-têtes plus un gestionnaire
de phase de pré-accès, 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. Chaque autre directive est héritée indépendamment, donc un
location imbriqué qui en définit une ne perd pas le reste — c'est toute la
différence par rapport à add_header.
cors_origin
Syntaxe : cors_origin * | any | <spécification> ... ;
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 Access-Control-Allow-Origin: * littéral. Identique pour tout le monde, donc aucun Vary: Origin n'est ajouté. Ne peut pas être combiné avec cors_credentials on. |
any |
Reflète quelle que soit l'Origin reçue. Compatible avec les identifiants, é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 insensible à 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 caractères génériques sont délibérément stricts : le * doit venir immédiatement 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 caractère générique correspond à un nombre quelconque
d'étiquettes 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'un iframe en bac à sable, un document data: ou une page file://
envoie — 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 feront correspondre dans
ce cas. Refléter null avec des identifiants donnerait à chaque frame en bac à sable
sur internet une lecture authentifiée de la réponse.
cors_methods
Syntaxe : cors_methods * | <méthode> ... ;
Défaut : cors_methods GET HEAD POST OPTIONS ;
Contexte : http, server, location, if in location
La valeur 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 | <nom> ... ;
Défaut : — (l'en-tête est omis)
Contexte : http, server, location, if in location
La valeur Access-Control-Allow-Headers envoyée lors d'un preflight. any renvoie en écho
l'Access-Control-Request-Headers de la requête telle quelle ; 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 <nom> ... ;
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 par script, au-delà de l'ensemble sûr. 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, permettant les cookies et l'authentification
HTTP sur les requêtes cross-origin.
La spécification CORS interdit de combiner les identifiants avec un caractère générique, 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] ... ne peut pas être combiné avec "cors_origin *"
Utilisez une liste explicite, ou cors_origin any pour refléter. Notez que any plus
les identifiants permet à n'importe quel site web de lire des réponses authentifiées depuis cet emplacement ;
c'est autorisé, et un avertissement est journalisé au démarrage.
cors_max_age
Syntaxe : cors_max_age <temps> ;
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
Indique s'il faut répondre aux preflights correspondants en interne. Désactivez-le lorsque votre
application implémente OPTIONS elle-même et que vous voulez seulement que les en-têtes de réponse
soient ajoutés.
Un preflight est court-circuité uniquement lorsqu'il en est réellement un — un OPTIONS
portant à la fois Origin et Access-Control-Request-Method — et que l'origine
correspond. Tout le reste passe à travers sans modification, donc WebDAV et les OPTIONS
au niveau application continuent de fonctionner, et un preflight depuis une origine non autorisée ne reçoit simplement
aucun Access-Control-Allow-Origin, ce qui fait que le navigateur le rejette.
Parce que le gestionnaire s'exécute dans la phase de pré-accès, un preflight est traité avant
que auth_basic, auth_request et deny aient une chance de le rejeter. C'est
délibéré : les navigateurs n'envoient jamais d'identifiants sur un preflight, donc toute authentification
devant lui casserait complètement le CORS. Les requêtes réelles vers le même emplacement
sont toujours authentifiées normalement.
cors_vary
Syntaxe : cors_vary on | off ;
Défaut : cors_vary on ;
Contexte : http, server, location, if in location
Indique s'il faut émettre Vary: Origin lorsque la réponse dépend de l'origine de la requête.
Laissez-le 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 aucune Origin du tout,
car une copie en cache de cette réponse ne doit jamais être rejouée à 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 cet emplacement, ou
si votre CDN indexe déjà sur Origin.
Remarques
- Un
Varydéjà présent sur la réponse est étendu, jamais remplacé : un amont envoyantVary: Accept-Languageressort commeVary: Accept-Language, Origin, et plusieurs lignesVaryamont sont fusionnées en une seule. UnVary: *est laissé tel quel, car selon la RFC 9110 il englobe 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 emplacement, 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 chaque cache la gère, donc c'est correct — juste pas soigné. Chargeznginx-module-compression-varysi vous voulez que tout soit fusionné en une seule ligne. - Ne définissez pas également les en-têtes CORS avec
add_headerdans le même emplacement. Ce module remplace toutAccess-Control-Allow-Originqu'il trouve — deux d'entre eux constituent un échec critique 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.