Aller au contenu

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_header est hérité-ou-remplacé, par niveau. Dès qu'un location ajoute un seul en-tête qui lui est propre, tous les add_header dé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 OPTIONS doit être traité avec un 204 et les bons en-têtes, sans atteindre votre application. Le faire à la main implique un bloc if, et il interagit mal avec try_files et proxy_pass.
  • add_header ne se déclenche que sur une liste blanche de codes de statut sauf si vous passez always. 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: Origin est 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-Methodet 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 Vary déjà présent sur la réponse est étendu, jamais remplacé : un amont envoyant Vary: Accept-Language ressort comme Vary: Accept-Language, Origin, et plusieurs lignes Vary amont sont fusionnées en une seule. Un Vary: * est laissé tel quel, car selon la RFC 9110 il englobe déjà tout.
  • Deux choses ajoutent leur Vary après ce module et arrivent donc comme une ligne de champ séparée plutôt que d'être fusionnées : un add_header Vary ... dans le même emplacement, et le Vary: Accept-Encoding de gzip_vary on. Plusieurs lignes de champ Vary signifient 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é. Chargez nginx-module-compression-vary si vous voulez que tout soit fusionné en une seule ligne.
  • Ne définissez pas également les en-têtes CORS avec add_header dans le même emplacement. Ce module remplace tout Access-Control-Allow-Origin qu'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.