cors: CORS correto para NGINX, incluindo preflight e Vary
Requer o plano Pro (ou superior) da assinatura GetPageSpeed NGINX Extras.
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 do /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 lida com as partes que a receita do 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 em quatro aspectos específicos:
add_headeré herdado-ou-substituído, por nível. No momento em que qualquerlocationadiciona um único cabeçalho próprio, todos osadd_headerdefinidos acima dela — incluindo seus cabeçalhos CORS — desaparecem silenciosamente. Nada avisa você.- Preflight precisa de um curto-circuito. Um preflight
OPTIONSprecisa ser respondido com um204e os cabeçalhos corretos, sem chegar à sua aplicação. Fazer isso manualmente significa um blocoif, e isso interage mal comtry_fileseproxy_pass. add_headersó é acionado em uma lista de códigos de status específicos a menos que você passealways. Então suas respostas de erro perdem os cabeçalhos CORS, e o navegador relata uma falha CORS opaca em vez do 404 ou 502 que realmente aconteceu.Vary: Originé esquecido. Quando a resposta depende da origem da solicitação e você não informa isso, qualquer cache compartilhado ou CDN à sua frente servirá alegremente a resposta de uma origem para outra. Isso é um bug de envenenamento de cache, e é o mais comum na prática.
Este módulo faz todas as quatro coisas corretamente, como um filtro de cabeçalho mais um manipulador
de fase de pré-acesso, 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 CORS. Todas as outras diretivas são herdadas independentemente, então uma
location aninhada 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 | <especificação> ...;
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 especificação:
| 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 curinga. |
~^https://(a\|b)\.example\.com$ |
Expressão regular. ~* para não diferenciar maiúsculas de minúsculas. |
Várias especificações podem ser listadas. Quando uma corresponde, essa origem é ecoada de volta.
* e any não podem ser misturados com outros valores.
Curingas são deliberadamente estritos: o * deve vir imediatamente após :// e
deve ser seguido por um .. https://*example.com é rejeitado na inicialização em vez de
corresponder silenciosamente a https://evilexample.com. Um curinga corresponde a qualquer número
de rótulos iniciais (https://a.b.example.com corresponde a https://*.example.com),
mas não corresponde ao apex simples e não ignora uma porta — uma origem com uma
porta precisa de uma entrada exata ou 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 explícita null na lista quando
cors_credentials on. Nem any nem uma expressão regular corresponderão a ele nesse
caso. Refletir null com credenciais daria a cada frame em sandbox
na internet uma leitura autenticada da resposta.
cors_methods
Sintaxe: cors_methods * | <método> ...;
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 | <nome> ...;
Padrão: — (o cabeçalho é 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 solicitação de volta literalmente; o cabeçalho é omitido
quando a solicitação não pediu nenhum. * não pode ser combinado com
cors_credentials on.
cors_expose_headers
Sintaxe: cors_expose_headers <nome> ...;
Padrão: — (o cabeçalho é omitido)
Contexto: http, server, location, if in location
Cabeçalhos de resposta que o navegador deve tornar legíveis para script, além do conjunto de lista segura. Enviados 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 solicitações de origem cruzada.
A especificação CORS proíbe combinar credenciais com um curinga, e todo navegador aplica 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ê publicar isso:
cors_credentials on;
cors_origin *; # nginx: [emerg] ... cannot be combined with "cors_origin *"
Use uma lista explícita, ou cors_origin any para refletir. Observe 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 <tempo>;
Padrão: — (o cabeçalho é 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 cabeçalho.
cors_preflight
Sintaxe: cors_preflight on | off;
Padrão: cors_preflight on;
Contexto: http, server, location, if in location
Se deve responder a preflights correspondentes internamente. Desative quando sua
aplicação implementar OPTIONS por conta própria e você quiser apenas que os cabeçalhos de resposta
sejam adicionados.
Um preflight é colocado em curto-circuito apenas quando é genuinamente um — um OPTIONS
carregando tanto Origin quanto Access-Control-Request-Method — e a origem
corresponde. Todo o resto passa direto sem alterações, 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 manipulador é executado na fase de pré-acesso, 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 completamente. Solicitaçõ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 solicitação.
Deixe isso ligado. É emitido sempre que a política depende da origem — incluindo
quando a origem não correspondeu, e quando a solicitação não veio com Origin algum,
porque uma cópia em cache dessa resposta nunca deve ser reproduzida para uma solicitação cuja
origem teria produzido cabeçalhos diferentes. Deliberadamente não é emitido
para um cors_origin * estático, que é o mesmo para todos e não varia.
Desligue apenas se você souber que nenhum cache compartilhado está à frente desta location, ou
se seu CDN já usa Origin como chave.
Notas
- Um
Varyjá presente na resposta é estendido, nunca substituído: um upstream enviandoVary: Accept-Languageresulta emVary: Accept-Language, Origin, e várias linhasVarydo upstream são dobradas em uma. UmVary: *é deixado como está, pois de acordo com a RFC 9110 ele já abrange tudo. - Duas coisas adicionam seu
Varydepois deste módulo e, portanto, 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 isso está correto — apenas não é organizado. Carreguenginx-module-compression-varyse quiser que tudo seja dobrado em uma linha. - Não defina também cabeçalhos CORS com
add_headerna mesma location. Este módulo substitui qualquerAccess-Control-Allow-Originque encontrar — dois deles é uma falha grave em todos os navegadores — mas os outros cabeçalhos acabariam duplicados. - Os cabeçalhos são emitidos em respostas 304 e 206, bem como em erros.
- Sub-requests são ignorados, correspondendo ao comportamento do
add_header.