cors: CORS correcto para NGINX, incluyendo preflight y Vary
Requiere el plan Pro (o superior) de la suscripción GetPageSpeed NGINX Extras.
Instalación
Puede instalar este módulo en cualquier distribución basada en RHEL, incluyendo, pero sin limitarse a:
- RedHat Enterprise Linux 7, 8, 9 y 10
- CentOS 7, 8, 9
- AlmaLinux 8, 9
- Rocky Linux 8, 9
- Amazon Linux 2 y 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
Habilite el módulo añadiendo lo siguiente al principio de /etc/nginx/nginx.conf:
load_module modules/ngx_http_cors_module.so;
Este documento describe nginx-module-cors v1.0.0 publicado el 15 de agosto de 2026.
CORS para NGINX que maneja las partes que la receta add_header hace mal.
¿Por qué no usar simplemente add_header?
La receta map $http_origin + add_header Access-Control-Allow-* es la que
todo el mundo copia, y está rota de cuatro maneras concretas:
add_headerse hereda o se reemplaza, por nivel. En el momento en que cualquierlocationañade una sola cabecera propia, todas lasadd_headerdefinidas por encima — incluyendo sus cabeceras CORS — desaparecen silenciosamente. Nada le avisa.- El preflight necesita un cortocircuito. Una petición
OPTIONSde preflight debe responderse con un204y las cabeceras correctas, sin llegar a su aplicación. Hacerlo a mano implica un bloqueif, e interactúa mal contry_filesyproxy_pass. add_headersolo se activa con una lista blanca de códigos de estado a menos que pasealways. Así que sus respuestas de error pierden sus cabeceras CORS, y el navegador reporta un fallo CORS opaco en lugar del 404 o 502 que realmente ocurrió.Vary: Originse olvida. Cuando la respuesta depende del origen de la petición y no lo indica, cualquier caché compartida o CDN delante suyo servirá alegremente la respuesta de un origen a otro. Esto es un bug de envenenamiento de caché, y es el más común en la práctica.
Este módulo hace las cuatro cosas correctamente, como un filtro de cabeceras más un manejador
de fase de preacceso, sin bloques if.
Sinopsis
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 entonces se ve así, y nunca llega a 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
Directivas
cors
Sintaxis: cors on | off;
Por defecto: cors off;
Contexto: http, server, location, if in location
Habilita el procesamiento CORS. Cada otra directiva se hereda de forma independiente, así que un
location anidado que establezca una de ellas no pierde el resto — que es toda la
diferencia respecto a add_header.
cors_origin
Sintaxis: cors_origin * | any | <spec> ...;
Por defecto: cors_origin *;
Contexto: http, server, location, if in location
Qué orígenes pueden leer el recurso. Dos palabras reservadas y tres formas de especificación:
| Valor | Significado |
|---|---|
* |
Emite un Access-Control-Allow-Origin: * literal. Igual para todos, así que no se añade Vary: Origin. No puede combinarse con cors_credentials on. |
any |
Refleja cualquier Origin que llegue. Seguro para credenciales, emite Vary: Origin. |
https://app.example.com |
Coincidencia exacta, insensible a mayúsculas. |
https://*.example.com |
Subdominio comodín. |
~^https://(a\|b)\.example\.com$ |
Expresión regular. ~* para insensible a mayúsculas. |
Se pueden listar varias especificaciones. Cuando una coincide, ese origen se devuelve.
* y any no pueden mezclarse con otros valores.
Los comodines son deliberadamente estrictos: el * debe ir justo después de :// y
debe ir seguido de un .. https://*example.com se rechaza al arrancar en lugar de
coincidir silenciosamente con https://evilexample.com. Un comodín coincide con cualquier número
de etiquetas iniciales (https://a.b.example.com coincide con https://*.example.com),
pero no coincide con el apex desnudo y no ignora un puerto — un origen con un
puerto necesita una entrada exacta o una expresión regular.
Origin: null — lo que envía un iframe en sandbox, un documento data: o una página file:// —
solo se empareja con una entrada null explícita en la lista cuando
cors_credentials on. Ni any ni una expresión regular lo emparejarán en
ese caso. Reflejar null con credenciales entregaría a cada frame en sandbox
de internet una lectura autenticada de la respuesta.
cors_methods
Sintaxis: cors_methods * | <method> ...;
Por defecto: cors_methods GET HEAD POST OPTIONS;
Contexto: http, server, location, if in location
El valor Access-Control-Allow-Methods enviado en un preflight. * no puede
combinarse con cors_credentials on.
cors_headers
Sintaxis: cors_headers * | any | <name> ...;
Por defecto: — (la cabecera se omite)
Contexto: http, server, location, if in location
El valor Access-Control-Allow-Headers enviado en un preflight. any devuelve
el Access-Control-Request-Headers de la petición tal cual; la cabecera se omite
cuando la petición no solicitó ninguna. * no puede combinarse con
cors_credentials on.
cors_expose_headers
Sintaxis: cors_expose_headers <name> ...;
Por defecto: — (la cabecera se omite)
Contexto: http, server, location, if in location
Cabeceras de respuesta que el navegador debería hacer legibles al script, más allá del conjunto de lista segura. Se envían en respuestas reales, no en preflights.
cors_credentials
Sintaxis: cors_credentials on | off;
Por defecto: cors_credentials off;
Contexto: http, server, location, if in location
Emite Access-Control-Allow-Credentials: true, permitiendo cookies y autenticación
HTTP en peticiones de origen cruzado.
La especificación CORS prohíbe emparejar credenciales con un comodín, y todos los navegadores lo aplican — así que una configuración que hace ambas cosas es una configuración cuyo CORS nunca funciona. NGINX se niega a arrancar en lugar de dejarle publicarla:
cors_credentials on;
cors_origin *; # nginx: [emerg] ... cannot be combined with "cors_origin *"
Use una lista explícita, o cors_origin any para reflejar. Tenga en cuenta que any más
credenciales permite que cualquier sitio web lea respuestas autenticadas de esa ubicación;
está permitido, y registra una advertencia al arrancar.
cors_max_age
Sintaxis: cors_max_age <time>;
Por defecto: — (la cabecera se omite)
Contexto: http, server, location, if in location
Cuánto tiempo puede un navegador almacenar en caché el resultado del preflight. cors_max_age 0; es un
valor significativo y se emite; omitir la directiva omite la cabecera.
cors_preflight
Sintaxis: cors_preflight on | off;
Por defecto: cors_preflight on;
Contexto: http, server, location, if in location
Si responder a preflights coincidentes internamente. Apáguelo cuando su
aplicación implemente OPTIONS por sí misma y solo quiera que se añadan las cabeceras de respuesta.
Un preflight se cortocircuita solo cuando es genuinamente uno — un OPTIONS
que lleva tanto Origin como Access-Control-Request-Method — y el origen
coincide. Todo lo demás pasa sin tocarse, así que WebDAV y los OPTIONS a nivel de
aplicación siguen funcionando, y un preflight de un origen no permitido simplemente no recibe
ningún Access-Control-Allow-Origin, que es lo que hace que el navegador lo rechace.
Como el manejador se ejecuta en la fase de preacceso, un preflight se responde antes de que
auth_basic, auth_request y deny tengan la oportunidad de rechazarlo. Esto es
deliberado: los navegadores nunca envían credenciales en un preflight, así que cualquier autenticación
delante de él rompería CORS por completo. Las peticiones reales a la misma ubicación
se siguen autenticando normalmente.
cors_vary
Sintaxis: cors_vary on | off;
Por defecto: cors_vary on;
Contexto: http, server, location, if in location
Si emitir Vary: Origin cuando la respuesta depende del origen de la petición.
Déjelo activado. Se emite siempre que la política depende del origen — incluyendo
cuando el origen no coincidió, y cuando la petición no llevaba ningún Origin en absoluto,
porque una copia en caché de esa respuesta nunca debe reproducirse a una petición cuyo
origen habría producido cabeceras diferentes. Deliberadamente no se emite
para un cors_origin * estático, que es igual para todos y no varía.
Apáguelo solo si sabe que no hay ninguna caché compartida delante de esta ubicación, o
si su CDN ya usa Origin como clave.
Notas
- Un
Varyya presente en la respuesta se extiende, nunca se reemplaza: un upstream que envíaVary: Accept-Languagesale comoVary: Accept-Language, Origin, y varias líneasVarydel upstream se pliegan en una. UnVary: *se deja solo, ya que según RFC 9110 ya lo subsume todo. - Dos cosas añaden su
Varydespués de este módulo y por tanto llegan como una línea de campo separada en lugar de plegarse: unadd_header Vary ...en la misma ubicación, y elVary: Accept-Encodingdegzip_vary on. Varias líneas de campoVarysignifican exactamente lo mismo que una línea combinada (RFC 9110 §5.3) y toda caché lo maneja, así que esto es correcto — solo que no es ordenado. Carguenginx-module-compression-varysi quiere que todo se pliegue en una sola línea. - No establezca también cabeceras CORS con
add_headeren la misma ubicación. Este módulo reemplaza cualquierAccess-Control-Allow-Originque encuentre — dos de ellos es un fallo duro en todos los navegadores — pero las otras cabeceras acabarían duplicadas. - Las cabeceras se emiten en respuestas 304 y 206 además de en errores.
- Las subpeticiones se omiten, coincidiendo con el comportamiento de
add_header.