Aller au contenu

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_header est hérité-ou-remplacé, par niveau. Dès qu'un location ajoute un seul en-tête de son propre chef, 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 répondu par un 204 et les bons en-têtes, sans atteindre votre application. Faire cela à la main implique un bloc if, et cela 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 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 Vary déjà présent sur la réponse est étendu, jamais remplacé : un upstream envoyant Vary: Accept-Language ressort en Vary: Accept-Language, Origin, et plusieurs lignes Vary d'upstream sont fusionnées en une seule. Un Vary: * est laissé tel quel, car selon la RFC 9110 il subsume 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 location, 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 tous les caches le gèrent, donc c'est correct — juste pas très propre. Chargez nginx-module-compression-vary si vous voulez que tout soit fusionné en une seule ligne.
  • Ne définissez pas aussi les en-têtes CORS avec add_header dans le même location. Ce module remplace tout Access-Control-Allow-Origin qu'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.