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 qualquerlocationadiciona um único header próprio, todoadd_headerdefinido acima dele — incluindo seus headers de CORS — desaparece silenciosamente. Nada te avisa.- Preflight precisa de um curto-circuito. Um preflight
OPTIONSprecisa ser respondido com um204e os headers corretos, sem chegar à sua aplicação. Fazer isso manualmente significa um blocoif, e ele interage mal comtry_fileseproxy_pass. add_headersó dispara em uma whitelist de códigos de status a menos que você passealways. 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
Varyjá presente na resposta é estendido, nunca substituído: um upstream enviandoVary: Accept-Languagesai comoVary: Accept-Language, Origin, e várias linhasVarydo upstream são dobradas em uma. UmVary: *é deixado em paz, já que pela RFC 9110 ele já subsume tudo. - Duas coisas adicionam seu
Varydepois deste módulo e então chegam como uma linha de campo separada em vez de serem dobradas: umadd_header Vary ...na mesma location, e oVary: Accept-Encodingdogzip_vary on. Várias linhas de campoVarysignificam 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. Carreguenginx-module-compression-varyse você quiser tudo dobrado em uma linha. - Não defina também headers de CORS com
add_headerna mesma location. Este módulo substitui qualquerAccess-Control-Allow-Originque 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.