Pular para conteúdo

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 qualquer location adiciona um único cabeçalho próprio, todos os add_header definidos acima dela — incluindo seus cabeçalhos CORS — desaparecem silenciosamente. Nada avisa você.
  • Preflight precisa de um curto-circuito. Um preflight OPTIONS precisa ser respondido com um 204 e os cabeçalhos corretos, sem chegar à sua aplicação. Fazer isso manualmente significa um bloco if, e isso interage mal com try_files e proxy_pass.
  • add_header só é acionado em uma lista de códigos de status específicos a menos que você passe always. 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-Methode 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 Vary já presente na resposta é estendido, nunca substituído: um upstream enviando Vary: Accept-Language resulta em Vary: Accept-Language, Origin, e várias linhas Vary do upstream são dobradas em uma. Um Vary: * é deixado como está, pois de acordo com a RFC 9110 ele já abrange tudo.
  • Duas coisas adicionam seu Vary depois deste módulo e, portanto, 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 isso está correto — apenas não é organizado. Carregue nginx-module-compression-vary se quiser que tudo seja dobrado em uma linha.
  • Não defina também cabeçalhos CORS com add_header na mesma location. Este módulo substitui qualquer Access-Control-Allow-Origin que 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.