Pular para conteúdo

cors: CORS correto para NGINX, incluindo preflight e Vary

Instalação

Você pode instalar este módulo em qualquer distribuição baseada em RHEL, incluindo, mas não se limitando a:

  • RedHat Enterprise Linux 7, 8, 9 e 10
  • CentOS 7, 8, 9
  • AlmaLinux 8, 9
  • Rocky Linux 8, 9
  • Amazon Linux 2 e 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 o módulo adicionando o seguinte no topo de /etc/nginx/nginx.conf:

load_module modules/ngx_http_cors_module.so;

Este documento descreve o nginx-module-cors v1.0.0 lançado em 15 de agosto de 2026.


CORS para NGINX que trata as partes que a receita com add_header faz errado.

Por que não usar apenas add_header?

A receita map $http_origin + add_header Access-Control-Allow-* é a que todo mundo copia, e ela está quebrada de quatro maneiras específicas:

  • add_header é herdado-ou-substituído, por nível. No momento em que qualquer location adiciona um único header próprio, todo add_header definido acima dele — incluindo seus headers de CORS — desaparece silenciosamente. Nada te avisa.
  • Preflight precisa de um curto-circuito. Um preflight OPTIONS precisa ser respondido com um 204 e os headers corretos, sem chegar à sua aplicação. Fazer isso manualmente significa um bloco if, e ele interage mal com try_files e proxy_pass.
  • add_header só dispara em uma whitelist de códigos de status a menos que você passe always. Então suas respostas de erro perdem os headers de CORS, e o navegador reporta uma falha opaca de CORS em vez do 404 ou 502 que de fato aconteceu.
  • Vary: Origin é esquecido. Quando a resposta depende da origem da requisição e você não diz isso, qualquer cache compartilhado ou CDN à sua frente vai alegremente servir a resposta de uma origem para outra. Este é um bug de envenenamento de cache, e é o mais comum por aí.

Este módulo faz todos os quatro corretamente, como um filtro de header mais um handler na fase de preaccess, sem blocos if.

Sinopse

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;
}

Um preflight então se parece com isto, e nunca chega ao 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

Diretivas

cors

Sintaxe: cors on | off;

Padrão: cors off;

Contexto: http, server, location, if in location

Habilita o processamento de CORS. Todas as outras diretivas são herdadas independentemente, então um location aninhado que define uma delas não perde as demais — que é toda a diferença em relação ao add_header.

cors_origin

Sintaxe: cors_origin * | any | <spec> ...;

Padrão: cors_origin *;

Contexto: http, server, location, if in location

Quais origens podem ler o recurso. Duas palavras reservadas e três formas de spec:

Valor Significado
* Emite um Access-Control-Allow-Origin: * literal. O mesmo para todos, então nenhum Vary: Origin é adicionado. Não pode ser combinado com cors_credentials on.
any Reflete qualquer Origin que chegar. Seguro para credenciais, emite Vary: Origin.
https://app.example.com Correspondência exata, sem diferenciar maiúsculas de minúsculas.
https://*.example.com Subdomínio com wildcard.
~^https://(a\|b)\.example\.com$ Expressão regular. ~* para não diferenciar maiúsculas de minúsculas.

Várias specs podem ser listadas. Quando uma corresponde, aquela origem é ecoada de volta. * e any não podem ser misturados com outros valores.

Os wildcards são deliberadamente estritos: o * deve vir logo após :// e deve ser seguido por um .. https://*example.com é rejeitado na inicialização em vez de silenciosamente corresponder a https://evilexample.com. Um wildcard corresponde a qualquer número de labels iniciais (https://a.b.example.com corresponde a https://*.example.com), mas não corresponde ao apex puro e não ignora uma porta — uma origem com porta precisa de uma entrada exata ou de uma expressão regular.

Origin: null — o que um iframe em sandbox, um documento data: ou uma página file:// envia — só é correspondido por uma entrada null explícita na lista quando cors_credentials on. Nem any nem uma expressão regular vão correspondê-lo nesse caso. Refletir null com credenciais entregaria a todo frame em sandbox na internet uma leitura autenticada da resposta.

cors_methods

Sintaxe: cors_methods * | <method> ...;

Padrão: cors_methods GET HEAD POST OPTIONS;

Contexto: http, server, location, if in location

O valor de Access-Control-Allow-Methods enviado em um preflight. * não pode ser combinado com cors_credentials on.

cors_headers

Sintaxe: cors_headers * | any | <name> ...;

Padrão: — (o header é omitido)

Contexto: http, server, location, if in location

O valor de Access-Control-Allow-Headers enviado em um preflight. any ecoa o Access-Control-Request-Headers da requisição de volta literalmente; o header é omitido quando a requisição não pediu nenhum. * não pode ser combinado com cors_credentials on.

cors_expose_headers

Sintaxe: cors_expose_headers <name> ...;

Padrão: — (o header é omitido)

Contexto: http, server, location, if in location

Headers de resposta que o navegador deve tornar legíveis para scripts, além do conjunto safelisted. Enviado em respostas reais, não em preflights.

cors_credentials

Sintaxe: cors_credentials on | off;

Padrão: cors_credentials off;

Contexto: http, server, location, if in location

Emite Access-Control-Allow-Credentials: true, permitindo cookies e autenticação HTTP em requisições cross-origin.

A especificação de CORS proíbe emparelhar credenciais com um wildcard, e todo navegador impõe isso — então uma configuração que faz ambos é uma configuração cujo CORS nunca funciona. O NGINX se recusa a iniciar em vez de deixar você publicá-la:

cors_credentials on;
cors_origin      *;      # nginx: [emerg] ... cannot be combined with "cors_origin *"

Use uma lista explícita, ou cors_origin any para refletir. Note que any mais credenciais permite que qualquer site leia respostas autenticadas daquela location; isso é permitido, e registra um aviso na inicialização.

cors_max_age

Sintaxe: cors_max_age <time>;

Padrão: — (o header é omitido)

Contexto: http, server, location, if in location

Por quanto tempo um navegador pode armazenar em cache o resultado do preflight. cors_max_age 0; é um valor significativo e é emitido; omitir a diretiva omite o header.

cors_preflight

Sintaxe: cors_preflight on | off;

Padrão: cors_preflight on;

Contexto: http, server, location, if in location

Se deve responder preflights correspondentes internamente. Desligue quando sua aplicação implementa OPTIONS por conta própria e você só quer os headers de resposta adicionados.

Um preflight só é curto-circuitado quando é genuinamente um — um OPTIONS carregando tanto Origin quanto Access-Control-Request-Method — e a origem corresponde. Todo o resto passa intacto, então WebDAV e OPTIONS de nível de aplicação continuam funcionando, e um preflight de uma origem não permitida simplesmente recebe nenhum Access-Control-Allow-Origin, que é o que faz o navegador rejeitá-lo.

Como o handler roda na fase de preaccess, um preflight é respondido antes que auth_basic, auth_request e deny tenham a chance de rejeitá-lo. Isso é deliberado: navegadores nunca enviam credenciais em um preflight, então qualquer autenticação à sua frente quebraria o CORS por completo. Requisições reais para a mesma location ainda são autenticadas normalmente.

cors_vary

Sintaxe: cors_vary on | off;

Padrão: cors_vary on;

Contexto: http, server, location, if in location

Se deve emitir Vary: Origin quando a resposta depende da origem da requisição.

Deixe isto ligado. É emitido sempre que a política depende da origem — incluindo quando a origem não correspondeu, e quando a requisição não carregava nenhum Origin, porque uma cópia em cache daquela resposta nunca deve ser reproduzida para uma requisição cuja origem teria produzido headers diferentes. É deliberadamente não emitido para um cors_origin * estático, que é o mesmo para todos e não varia.

Desligue apenas se você sabe que nenhum cache compartilhado está à frente desta location, ou seu CDN já usa Origin como chave.

Notas

  • Um Vary já presente na resposta é estendido, nunca substituído: um upstream enviando Vary: Accept-Language sai como Vary: Accept-Language, Origin, e várias linhas Vary do upstream são dobradas em uma. Um Vary: * é deixado em paz, já que pela RFC 9110 ele já subsume tudo.
  • Duas coisas adicionam seu Vary depois deste módulo e então chegam como uma linha de campo separada em vez de serem dobradas: um add_header Vary ... na mesma location, e o Vary: Accept-Encoding do gzip_vary on. Várias linhas de campo Vary significam exatamente a mesma coisa que uma linha combinada (RFC 9110 §5.3) e todo cache lida com isso, então isto está correto — apenas não está organizado. Carregue nginx-module-compression-vary se você quiser tudo dobrado em uma linha.
  • Não defina também headers de CORS com add_header na mesma location. Este módulo substitui qualquer Access-Control-Allow-Origin que encontrar — dois deles é uma falha grave em todo navegador — mas os outros headers acabariam duplicados.
  • Headers são emitidos em respostas 304 e 206, bem como em erros.
  • Subrequests são ignoradas, correspondendo ao comportamento do add_header.