Skip to content
Voltar ao blog
Tutoriais

Prioridade de location no Nginx: ordem de correspondência

Como o nginx escolhe um bloco location: a ordem exata para =, ^~, ~ e prefixos, além das armadilhas que quebram configurações, com um testador online grátis.

12 min de leitura

Prioridade de location no Nginx: ordem de correspondência

O nginx não lê seus blocos location de cima para baixo parando no primeiro que serve. Só esse mal-entendido já responde pela maioria dos relatos de “meu bloco location não funciona”. Para locations de prefixo, a posição do bloco dentro do arquivo não muda nada: o nginx compara todos e fica com a correspondência mais longa.

A prioridade de location no nginx é uma sequência fixa de quatro etapas:

  1. Correspondência exata. Se um location = /path for igual à URI, o nginx usa esse bloco e encerra a busca. Nem a comparação de prefixo nem as regexes chegam a rodar.
  2. Prefixo mais longo. Todo location de prefixo (location /path e location ^~ /path) com que a URI comece entra na comparação. O mais longo é memorizado, mas ainda não é usado.
  3. O curto-circuito do ^~. Se o prefixo memorizado tiver ^~, o nginx pula a fase de regex inteira e usa aquele bloco.
  4. Regex, na ordem do arquivo. Caso contrário, os locations ~ e ~* são testados na ordem em que aparecem na configuração, e a primeira correspondência vence. Se nenhuma casar, vale o prefixo memorizado na etapa 2.

Duas dessas regras puxam para lados opostos: prefixos são escolhidos por comprimento, ignorando a ordem; regexes, por ordem, ignorando o comprimento. Ler a configuração de cima para baixo nunca vai revelar esse conflito. Se você quer a resposta para o seu próprio arquivo, e não para os exemplos abaixo, cole-o no testador de nginx location gratuito. Ele repete essa sequência e mostra em que etapa cada bloco perdedor caiu fora. Ele roda no seu navegador, então uma configuração de produção colada aqui nunca sai da página. Cada regra de correspondência descrita aqui foi verificada em um nginx 1.27.5 em execução, não copiada de artigos de segunda mão.

A prioridade de location no nginx em resumo

Cinco modificadores participam da correspondência de URI, e mais um que não participa.

ModificadorSintaxeCasa porEncerra a fase de regexUso típico
=location = /pathIgualdadeSimRotas quentes como / ou /favicon.ico
^~location ^~ /pathComeça comSim, se for o prefixo mais longoDiretórios que jamais podem chegar a uma regex
~location ~ regexPCRE, diferencia maiúsculasNãoRoteamento por extensão quando a caixa importa
~*location ~* regexPCRE, ignora maiúsculasNãoRoteamento por extensão quando a caixa não importa
(nenhum)location /pathComeça comNãoRoteamento geral por caminho
@location @nameNunca casa com uma URIAlvos de error_page e try_files

A regra de bolso: uma correspondência exata ganha de tudo, uma regex ganha de um prefixo, e prefixos disputam entre si pelo comprimento. A única exceção é ^~, e ela é bem mais estreita do que parece.

Essa tabela é um ranking só no sentido mais frouxo do termo. O ^~ aparece acima do ~, e mesmo assim um bloco ^~ perde para uma regex com frequência, porque o modificador só é consultado no prefixo que já venceu pelo comprimento.

O algoritmo de seleção em quatro etapas

A ordem de correspondência fica mais fácil de aprender a partir de uma configuração que exercita todas as regras de uma vez. Este é o exemplo da documentação do nginx, e vale a pena memorizá-lo:

server {
    location = /                   { return 200 "A\n"; }
    location /                     { return 200 "B\n"; }
    location /documents/           { return 200 "C\n"; }
    location ^~ /images/           { return 200 "D\n"; }
    location ~* \.(gif|jpg|jpeg)$  { return 200 "E\n"; }
}

Cinco requisições, cinco respostas diferentes:

RequisiçãoVencedorPor quê
/ACorrespondência exata. A busca termina na hora.
/index.htmlBNenhuma regex casou, então vale o prefixo memorizado.
/documents/document.htmlCPrefixo mais longo que /.
/images/1.gifDO ^~ venceu a etapa de prefixo, então a regex nem chegou a rodar.
/documents/1.jpgEA regex passou na frente de um prefixo mais longo que não tinha ^~.

Compare as duas últimas linhas. /images/ e /documents/ são ambos prefixos, ambos casam e ambos são a correspondência mais longa para a sua requisição. Uma requisição vai para o bloco de prefixo e a outra vai para a regex. A única diferença são dois caracteres.

A etapa 2 é a parte que as pessoas pulam: o nginx não usa o prefixo mais longo, ele o memoriza. O bloco é um candidato, e a fase de regex ainda pode tomar a requisição dele. Só as etapas 1, 3 e 4 encerram a busca.

Por que “prefixo mais longo” se mede em caracteres, não em segmentos de caminho

A comparação de prefixo é uma comparação de strings pura e simples. Ela não sabe que / separa segmentos de caminho e não para em nenhuma fronteira. Dada esta configuração:

server {
    location /static  { }
    location /static/ { }
}

uma requisição para /staticfoo é atendida por location /static. A URI começa com esses sete caracteres, então casa. Já /static/ não casa de jeito nenhum, porque não há barra naquela posição. Uma requisição para /static/x vai para o outro lado e fica com /static/, o mais longo dos dois.

A consequência é que location /static também é dono de /static-backup, /staticfiles e de qualquer outra coisa que por acaso comece com as mesmas letras. Se você queria um diretório, escreva a barra final e adicione um location exato para o caminho sem ela:

server {
    location /static/ { root /var/www; }
    location = /static { return 301 /static/; }
}

A maioria dos tutoriais usa exemplos de caminho tão comportados que isso nunca aparece, e é por isso que passa batido no code review com tanta frequência. O testador de nginx location mostra todos os blocos que casaram e quantos caracteres cada um casou, então dá para ver na hora quando um prefixo está engolindo os vizinhos.

O que ^~ realmente significa (e o que não significa)

A explicação que circula por aí sobre o ^~ é que “ele faz este bloco ter prioridade sobre as regexes”. Isso é próximo o bastante da verdade para ser perigoso.

O que ^~ de fato faz durante a comparação de prefixos: absolutamente nada. Ele não alonga a correspondência nem muda qual prefixo é memorizado. Ele é verificado depois, sobre o único prefixo que já venceu pelo comprimento. Se esse vencedor tiver ^~, a fase de regex é pulada. Se não tiver, a fase de regex roda normalmente.

O que significa que um prefixo simples mais longo desativa o ^~ sem avisar:

server {
    location ^~ /a/    { }
    location /a/b/     { }
    location ~ \.php$  { }
}

Uma requisição para /a/b/x.php é tratada por ~ \.php$. /a/b/ é o prefixo que casa e é o mais longo, então é o que o nginx memoriza; como o modificador dele é simples, a fase de regex acontece, e a regex casa primeiro e leva a requisição. O bloco ^~ continua ali no arquivo, com toda a cara de proteção, sem nenhuma influência sobre essa requisição.

Troque a URI para /a/x.php e a mesma configuração se comporta de forma completamente diferente: agora ^~ /a/ é a correspondência mais longa, a fase de regex é pulada e o bloco ^~ vence. O arquivo é o mesmo, a requisição tem o mesmo formato e o resultado é o oposto.

Isso tem consequência prática bem direta. O uso clássico do ^~ é manter um diretório com permissão de escrita longe de um interpretador:

server {
    location ^~ /uploads/ { }
    location ~ \.php$     { fastcgi_pass unix:/run/php-fpm.sock; }
}

Tire o ^~ e uma requisição para /uploads/evil.php vai direto para o PHP-FPM. É daí que sai uma longa série de relatos de upload que vira RCE, e o que separa vulnerável de seguro são dois caracteres. É também por isso que o caso do “^~ derrotado” importa: adicionar um prefixo simples mais longo, como location /uploads/thumbs/, reabre o buraco para tudo que estiver abaixo dele, e o diff que faz isso parece completamente inofensivo.

Repare também no escopo. O ^~ só suprime regexes declaradas no próprio nível; ele nunca suprime regexes aninhadas dentro do próprio bloco, e um ^~ em um location aninhado não protege contra uma regex declarada no nível do server. Ligue e desligue o modificador no testador de nginx location e veja o vencedor mudar. As regexes puladas continuam na tabela, com o rótulo dizendo que foram puladas.

Locations com regex: a ordem vence a especificidade

Uma regex de location no nginx usa ~ para diferenciar maiúsculas de minúsculas e ~* para ignorar a caixa. A correspondência por prefixo, em contraste, sempre diferencia maiúsculas no Linux. (Em sistemas de arquivos que ignoram a caixa, como o do macOS, o nginx compara prefixos ignorando maiúsculas e força todo location com regex a se comportar como ~*. Se você desenvolve no Mac e implanta no Linux, essa diferença consegue esconder uma regra quebrada até ela chegar em produção.)

A regra que pega as pessoas é esta: as regexes são avaliadas na ordem em que aparecem no arquivo de configuração, e a primeira correspondência encerra a busca. Especificidade, comprimento e ancoragem não têm nenhuma influência sobre a ordem.

server {
    location ~ ^/a       { }
    location ~ ^/a/b/c$  { }
}

Uma requisição para /a/b/c é levada por ~ ^/a. O segundo bloco casa com a URI exatamente, é muito mais preciso e nunca vai executar para requisição nenhuma. É configuração morta que o nginx -t aceita sem dizer uma palavra.

Daí o hábito: ordene as regexes da mais específica para a mais genérica e mantenha a lista curta. Um padrão amplo perto do topo torna inalcançável tudo que vem abaixo dele e, como um diff que apenas reordena linhas parece inofensivo, a regressão costuma chegar durante uma faxina, não durante uma feature.

A ancoragem merece o mesmo cuidado. location ~ /admin não está ancorado em nenhuma das pontas, então procura em qualquer posição da URI e casa alegremente com /public/admin/x. Escreva ~ ^/admin quando você quer dizer o começo. Ancorar só no fim, como em ~ \.php$, é normal e correto para roteamento por extensão.

Alguns detalhes de PCRE que intuições formadas em JavaScript erram:

  • O nginx compila os padrões de location com PCRE, sem modo UTF nem multiline, então os padrões operam sobre bytes e ^ ancora apenas no início da URI.
  • O $ do PCRE também casa logo antes de uma quebra de linha final. Uma URI terminada em %0A ainda satisfaz \.php$, o que é uma forma conhecida de escapar de regras baseadas na extensão do arquivo.
  • Construções sem equivalente em JavaScript são comuns no PCRE: grupos atômicos (?>…), quantificadores possessivos a*+, modificadores inline como (?i), classes POSIX como [[:alpha:]] e escapes como \A, \z, \K e \Q…\E.
  • Um padrão que contenha { ou } precisa de aspas. location ~ ^/a{2}$ não carrega, com o erro unknown directive "2}$", porque a chave encerrou o token. Escreva location ~ "^/a{2}$".

Grupos de captura funcionam como você espera, e $1 em diante ficam disponíveis dentro do bloco:

upstream backend {
    server 127.0.0.1:8080;
}

server {
    location ~ ^/user/(\d+)/profile$ {
        proxy_pass http://backend/profiles/$1;
    }
}

Se você ainda está depurando o padrão em si, e não a posição dele no arquivo, teste antes no testador Regex; o cheat sheet de regex cobre a sintaxe em profundidade.

A etapa que todo mundo pula: a normalização da URI

Antes de consultar qualquer location, o nginx reescreve o alvo da requisição. Seus padrões são comparados com o caminho normalizado, não com os bytes que chegaram pela rede. Quase nenhum tutorial menciona isso, e é o que decide um número surpreendente dos casos de “meu location não casa”.

A normalização faz quatro coisas: separa a query string, decodifica os percentuais do caminho, resolve os segmentos . e .. e junta barras repetidas.

Alvo da requisição$uri normalizadaObservação
//a//x/a/xBarras repetidas unidas
/a/../b/x/b/x.. resolvido antes da correspondência
/a/b%2F..%2Fzz/a/zz%2F vira um separador de verdade e entra na resolução
/a/%2e%2e/b/x/b/x%2E vira um ponto que também participa
/a%20b/x/a b/x%20 vira um espaço de verdade
/a+b/x/a+b/x+ não é espaço dentro de um caminho
/a?x=/b/aA query string é separada primeiro
/a%3Fx=1/a?x=1%3F continua literal; a query string fica vazia

Três caracteres decodificados são exceção: %25, %23 e %3F voltam para o caminho literalmente, sem serem reinterpretados. É por isso que /a%3Fx=1 termina com um ponto de interrogação dentro do caminho e nada em $args.

Dois tipos de alvo nem chegam à seleção de location. Segmentos .. que sobem acima da raiz e escapes inválidos como %00 levam 400 antes de a correspondência começar.

É daí que sai a parte que interessa para segurança. Se você usa um bloco location como fronteira de controle de acesso, o caminho que você escreveu é comparado com o caminho resolvido:

server {
    location /a/ { }
    location /b/ { }
}

Uma requisição para /a/b%2F..%2Fzz não permanece sob /a/b/. Ela normaliza para /a/zz e é tratada por location /a/. Raciocinar sobre o alvo bruto em vez de sobre $uri dá a resposta errada aqui, e “resposta errada” em contexto de controle de acesso tem um nome específico. Antes de confiar em location /admin para proteger qualquer coisa, confirme qual é de fato o caminho normalizado. O testador de nginx location mostra o alvo bruto, a $uri normalizada e a query string separada como três linhas distintas; se o que você precisa é só raciocinar sobre a codificação em si, o Decodificador e Codificador de URL resolve isso isoladamente.

Outra consequência, sem rodeios: a query string nunca participa da correspondência. location /search?q= não consegue casar com uma requisição para /search?q=1, porque a seleção só enxerga /search. Para ramificar com base em um parâmetro, leia $arg_name dentro do bloco.

Cinco configurações que não fazem o que você imagina

Um bloco ^~ perdendo para um prefixo simples mais longo

O diretório com ^~ parece protegido e, ainda assim, uma regex atende requisições dentro dele. O motivo é que o ^~ só é consultado no prefixo que já venceu pelo comprimento: um prefixo simples mais longo é memorizado no lugar dele e não suprime coisa nenhuma. Ou você põe ^~ também no prefixo mais longo, ou tira o prefixo mais longo do arquivo.

# Broken: /a/b/x.php goes to the regex
location ^~ /a/    { }
location /a/b/     { }
location ~ \.php$  { }

# Fixed: /a/b/x.php goes to ^~ /a/b/
location ^~ /a/    { }
location ^~ /a/b/  { }
location ~ \.php$  { }

Um prefixo sem barra final capturando os vizinhos

Um bloco pensado para um diretório também atende caminhos que apenas começam com as mesmas letras, porque a correspondência por prefixo compara caracteres e não segmentos de caminho: location /app casa com /application do mesmo jeito. Escreva a barra final e, se o caminho sem ela também precisar ser tratado, adicione location = /app.

A regex específica colocada abaixo da genérica

A regra precisa nunca dispara e nenhum erro aparece em lugar nenhum. As regexes são testadas na ordem do arquivo e a primeira correspondência encerra a busca, então tudo que estiver abaixo de um padrão amplo fica inalcançável. Mova o padrão específico para cima do amplo, ou aperte o amplo com uma âncora.

Uma regex escrita depois de ^~

A regra de deny carrega sem problemas e não bloqueia nada. O ^~ recebe um prefixo literal, não um padrão, e o nginx não reclama disso: o bloco simplesmente nunca casa com uma URI. A correção é usar o modificador de regex.

# Broken: matches nothing, loads without error
location ^~ "\.php$" { deny all; }

# Fixed
location ~ \.php$ { deny all; }

Achar que a query string participa

O location que contém ? nunca casa, porque a query string é separada durante a normalização e a seleção de location roda apenas sobre o caminho. Case pelo caminho e inspecione $arg_name dentro do bloco.

location /search {
    if ($arg_q = "") { return 400; }
}

Depuração: descubra qual bloco realmente venceu

O log de debug é a resposta definitiva, e também o que dá mais trabalho para preparar. Ele exige um binário compilado com suporte a debug, então confira isso primeiro:

nginx -V 2>&1 | grep -o with-debug

Depois ative o log e filtre pela linha que nomeia o bloco selecionado:

error_log /var/log/nginx/debug.log debug;
grep "using configuration" /var/log/nginx/debug.log

Sondar cabeçalhos de resposta é mais rápido e dispensa um build com debug. Marque cada candidato e leia os cabeçalhos de volta:

location ^~ /uploads/ {
    add_header X-Debug-Location "uploads-caret" always;
    return 204;
}
location ~ \.php$ {
    add_header X-Debug-Location "php-regex" always;
    return 204;
}
curl -sI --path-as-is 'http://localhost/uploads/evil.php' | grep -i x-debug-location

O --path-as-is faz diferença: sem ele o curl prestativamente resolve os .. por você e você acaba testando uma URI diferente da que queria. Se você está montando algo mais elaborado, o Gerador de Comando cURL escreve as flags para você, e a folha de consulta curl cobre o resto. Quando a sondagem volta como um redirecionamento ou um 404 em vez do seu cabeçalho, a referência de códigos de status HTTP costuma dizer qual módulo produziu aquilo.

O nginx -T imprime a configuração totalmente mesclada, com arquivos de include e tudo. É assim que você descobre em que ordem suas regexes realmente estão depois de seis arquivos serem montados, que raramente é a ordem em que aparecem no arquivo que você estava editando.

nginx -T | grep -n "location"

Reproduza a falha no testador de nginx location antes de editar o servidor. Iterar contra uma configuração que você ainda não implantou é mais rápido que um ciclo de reload, e a tabela de decisão já diz em que etapa cada bloco foi eliminado.

Locations aninhados, try_files e o que eles não mudam

Locations aninhados rodam o mesmo algoritmo um nível abaixo. Assim que um location de prefixo vence, o nginx desce para os filhos dele e repete a busca ali, o que significa que uma regex aninhada é testada antes das regexes do nível pai:

server {
    location ~ \.php$ { }
    location /a/ {
        location ~ \.php$ { return 200 "nested\n"; }
    }
}

/a/x.php é tratado pelo bloco aninhado. O aninhamento também tem um efeito menos óbvio: ele pode tornar inalcançável um prefixo que é globalmente mais longo, porque só se desce para o vencedor de cada nível. Se location /a/bb/ estiver aninhado dentro de location /a/, e um irmão location /a/b estiver no nível externo, uma requisição para /a/bb/x vai para /a/b. A comparação externa acontece primeiro, e /a/b a vence. Uma descida que não encontra nada também não volta atrás; o pai fica com a requisição.

try_files e rewrite não fazem parte da seleção. Eles executam dentro do bloco que já venceu e não conseguem voltar atrás para mudar esse resultado. Se uma requisição nunca chega ao bloco que contém o seu try_files, a diretiva é irrelevante, e o culpado de sempre é uma regex ~ \.php$ que pega a requisição antes de o bloco de prefixo ter a sua vez. A exceção que vale conhecer: um redirecionamento interno (rewrite … last, ou um salto de error_page) reinicia a correspondência, então a URI reescrita é resolvida contra a lista de locations de novo, desde o topo.

Por fim, o 301 que você recebe ao pedir um diretório sem a barra final não é uma falha de correspondência. Dois mecanismos separados produzem isso. Se um location cujo nome termina em / tiver proxy_pass ou outra diretiva *_pass, uma requisição para o mesmo caminho sem a barra é respondida com um 301 durante a seleção, antes de qualquer regex ser avaliada, e a query string é preservada:

server {
    location /user/ { proxy_pass http://backend/; }
}
# GET /user?x=1  ->  301 to /user/?x=1

Adicionar location = /user suprime esse redirecionamento. Em separado, o módulo de arquivos estáticos emite o seu próprio 301 quando um caminho resolve para um diretório real em disco, o que depende do seu sistema de arquivos, não da sua configuração.

FAQ

Quais são os cinco modificadores de location do nginx?

Os cinco modificadores de location do nginx são = para correspondência exata, ^~ para um prefixo que pula a fase de regex, nenhum modificador para um prefixo comum, ~ para uma regex que diferencia maiúsculas e ~* para uma que ignora a caixa. Uma sexta forma, location @name, nunca participa da correspondência de URI e existe apenas como alvo de error_page e try_files.

Uma correspondência exata com = deixa o nginx mais rápido?

Um location de correspondência exata encerra a busca na hora, pulando a varredura de prefixos e toda avaliação de regex. A economia é real, mas pequena demais para ser percebida. Vale escrever para endpoints acessados milhares de vezes por segundo, como health checks ou /favicon.ico. Uma pilha de blocos = para páginas comuns custa mais em complexidade de configuração do que rende.

Posso escrever o modificador de location sem espaço, como ~*^/api?

Pode. location ~*^/api/ e location ~* ^/api/ significam exatamente a mesma coisa, porque o nginx retira o modificador do começo do nome e reconhece o modificador mais longo primeiro, de modo que ~* é identificado antes de ~. Ainda assim, mantenha o espaço. Um modificador colado parece parte do padrão e acaba mal interpretado por humanos durante a revisão.

Qual é a diferença entre root e alias dentro de um bloco location?

O root acrescenta a URI inteira ao diretório, enquanto o alias substitui o prefixo que casou por ele. Com location /static/ { root /var/www; }, uma requisição para /static/x.css procura por /var/www/static/x.css; troque para alias /var/www/assets/; e ela procura por /var/www/assets/x.css. Com alias, ou você põe barra final tanto no location quanto no caminho, ou não põe em nenhum dos dois.

Uma requisição pode corresponder a mais de um bloco location?

Vários blocos podem casar, mas exatamente um atende a requisição. O nginx compara todo location de prefixo e, quando necessário, todo location com regex, e então entrega a requisição ao único vencedor. As diretivas não são herdadas dos blocos perdedores: o que você precisa em todo lugar tem que morar no nível server ou http, ou ser repetido em cada bloco.

O nginx -t me diz qual location vai casar?

Não. O nginx -t verifica a sintaxe e a validade da configuração; ele nunca simula uma requisição, então não informa nada sobre a ordem de correspondência. Para descobrir qual bloco é dono de uma URI, leia o log de debug, adicione um cabeçalho de resposta temporário ou cole a configuração no testador de nginx location e leia por que cada bloco ganhou ou perdeu.

Tags: nginx web-server devops regex configuration

Artigos relacionados

Ver todos os artigos