Saltar a contenido

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_header se hereda o se reemplaza, por nivel. En el momento en que cualquier location añade una sola cabecera propia, todas las add_header definidas por encima — incluyendo sus cabeceras CORS — desaparecen silenciosamente. Nada le avisa.
  • El preflight necesita un cortocircuito. Una petición OPTIONS de preflight debe responderse con un 204 y las cabeceras correctas, sin llegar a su aplicación. Hacerlo a mano implica un bloque if, e interactúa mal con try_files y proxy_pass.
  • add_header solo se activa con una lista blanca de códigos de estado a menos que pase always. 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: Origin se 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-Methody 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 Vary ya presente en la respuesta se extiende, nunca se reemplaza: un upstream que envía Vary: Accept-Language sale como Vary: Accept-Language, Origin, y varias líneas Vary del upstream se pliegan en una. Un Vary: * se deja solo, ya que según RFC 9110 ya lo subsume todo.
  • Dos cosas añaden su Vary después de este módulo y por tanto llegan como una línea de campo separada en lugar de plegarse: un add_header Vary ... en la misma ubicación, y el Vary: Accept-Encoding de gzip_vary on. Varias líneas de campo Vary significan 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. Cargue nginx-module-compression-vary si quiere que todo se pliegue en una sola línea.
  • No establezca también cabeceras CORS con add_header en la misma ubicación. Este módulo reemplaza cualquier Access-Control-Allow-Origin que 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.