Skip to content
Torna al blog
Tutorial

Priorità location Nginx: ordine di corrispondenza

Come nginx sceglie davvero un blocco location: l'ordine esatto di =, ^~, ~ e prefissi, più le trappole più comuni, con un tester online gratuito.

12 min di lettura

Priorità location Nginx: l’ordine di corrispondenza spiegato

Nginx non legge i tuoi blocchi location dall’alto verso il basso fermandosi al primo che sembra andare bene. Quel singolo malinteso sta dietro alla maggior parte delle segnalazioni del tipo “il mio blocco location non funziona”. Per le location con prefisso, la posizione del blocco nel file non conta: nginx le confronta tutte e tiene la corrispondenza più lunga.

La priorità delle location in nginx è una sequenza fissa di quattro passaggi:

  1. Corrispondenza esatta. Se una location = /path è uguale all’URI, nginx la usa e si ferma. Niente confronto tra prefissi, niente regex.
  2. Prefisso più lungo. Vengono confrontate tutte le location con prefisso (location /path e location ^~ /path) con cui l’URI inizia. La più lunga viene ricordata, non ancora usata.
  3. Il corto circuito di ^~. Se il prefisso ricordato porta ^~, nginx salta del tutto la fase regex e usa quel blocco.
  4. Regex, nell’ordine del file. Altrimenti le location ~ e ~* vengono provate nell’ordine in cui compaiono nella configurazione, e vince la prima che corrisponde. Se non corrisponde nessuna, si usa il prefisso ricordato al passaggio 2.

Due di queste regole tirano in direzioni opposte: i prefissi si scelgono per lunghezza a prescindere dall’ordine, le regex per ordine a prescindere dalla lunghezza. Leggere una configurazione dall’alto verso il basso non farà mai emergere quel conflitto. Se vuoi la risposta per il tuo file e non per gli esempi qui sotto, incollalo nel tester nginx location gratuito: riproduce questa sequenza e mostra la fase in cui ogni blocco perdente è uscito di scena. Gira nel tuo browser, quindi una configurazione di produzione incollata qui non lascia mai la pagina. Ogni regola di corrispondenza descritta qui è stata verificata su un nginx 1.27.5 in esecuzione, non ripresa da articoli di seconda mano.

La priorità delle location nginx in sintesi

Alla corrispondenza degli URI partecipano cinque modificatori, più uno che non partecipa.

ModificatoreSintassiCorrisponde perFerma la fase regexUso tipico
=location = /pathUguaglianzaPercorsi caldi come / o /favicon.ico
^~location ^~ /pathInizio della stringaSì, se è il prefisso più lungoDirectory che non devono mai arrivare a una regex
~location ~ regexPCRE, sensibile alle maiuscoleNoRouting per estensione quando le maiuscole contano
~*location ~* regexPCRE, indifferente alle maiuscoleNoRouting per estensione quando non contano
(nessuno)location /pathInizio della stringaNoRouting generico per percorso
@location @nameMai in base all’URIDestinazioni di error_page e try_files

La regola pratica: una corrispondenza esatta batte tutto, una regex batte un prefisso e i prefissi si battono tra loro sulla lunghezza. L’unica eccezione è ^~, ed è più ristretta di quanto sembri.

Quella tabella è una classifica solo in senso molto lato. ^~ sta sopra ~, eppure un blocco ^~ perde regolarmente contro una regex, perché il modificatore viene consultato solo sul prefisso che ha già vinto sulla lunghezza.

L’algoritmo di selezione in quattro passaggi

L’ordine di corrispondenza delle location nginx si impara meglio da una configurazione che mette alla prova tutte le regole insieme. Questo è l’esempio della documentazione nginx, e conviene mandarlo a memoria:

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

Cinque richieste, cinque risposte diverse:

RichiestaVincitorePerché
/ACorrispondenza esatta. La ricerca finisce subito.
/index.htmlBNessuna regex ha corrisposto, quindi si usa il prefisso ricordato.
/documents/document.htmlCPrefisso più lungo di /.
/images/1.gifD^~ ha vinto la fase dei prefissi, quindi la regex non è mai partita.
/documents/1.jpgELa regex ha battuto un prefisso più lungo a cui mancava ^~.

Confronta le ultime due righe. /images/ e /documents/ sono entrambi prefissi, corrispondono entrambi, sono entrambi la corrispondenza più lunga per la loro richiesta. Una richiesta finisce al blocco con prefisso e l’altra alla regex. L’unica differenza sono due caratteri.

Il passaggio 2 è quello che si tende a saltare: il prefisso più lungo nginx non lo usa, se lo ricorda. Il blocco resta un candidato, e la fase regex può ancora portargli via la richiesta. Solo i passaggi 1, 3 e 4 chiudono la ricerca.

Perché il “prefisso più lungo” si misura in caratteri, non in segmenti di percorso

Il confronto tra prefissi è un semplice confronto tra stringhe. Non sa che / separa i segmenti di percorso e non si ferma a un confine. Data questa configurazione:

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

una richiesta per /staticfoo viene servita da location /static. L’URI inizia con quei sette caratteri, quindi corrisponde. /static/ non corrisponde affatto, perché in quella posizione non c’è nessuno slash. Una richiesta per /static/x va dalla parte opposta e prende /static/, il più lungo dei due.

La conseguenza è che location /static si prende anche /static-backup, /staticfiles e qualunque altra cosa cominci con le stesse lettere. Se intendevi una directory, scrivi lo slash finale e aggiungi una location esatta per il percorso nudo:

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

La maggior parte dei tutorial usa esempi di percorsi ordinati che non lo mettono mai in luce, ed è per questo che passa così spesso la code review. Il tester nginx location elenca ogni blocco che ha corrisposto e su quanti caratteri, così un prefisso che si mangia i vicini salta all’occhio.

Cosa significa davvero ^~ (e cosa non significa)

La descrizione più diffusa del modificatore ^~ di nginx è “dà a questo blocco la priorità sulle regex”. È abbastanza vicina al vero da risultare pericolosa.

Cosa fa davvero ^~: assolutamente nulla durante il confronto tra prefissi. Non allunga la corrispondenza, non solleva il blocco sopra gli altri prefissi e non cambia quale prefisso viene ricordato. Viene controllato dopo, sull’unico prefisso che ha già vinto sulla lunghezza. Se quel vincitore porta ^~, la fase regex viene saltata. Se non lo porta, la fase regex parte come sempre.

Il che significa che un prefisso semplice più lungo lo disattiva in silenzio:

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

Una richiesta per /a/b/x.php viene gestita da ~ \.php$. /a/b/ è il prefisso corrispondente più lungo, quindi è quello che nginx ricorda; il suo modificatore semplice permette la fase regex; la regex corrisponde per prima e si prende la richiesta. Il blocco ^~ è ancora nel file, sembra ancora protettivo e su questa richiesta non ha il minimo effetto.

Cambia l’URI in /a/x.php e la stessa configurazione si comporta in modo completamente diverso: ora ^~ /a/ è la corrispondenza più lunga, la fase regex viene saltata e vince il blocco ^~. Stesso file, richiesta della stessa forma, risultato opposto.

Non è una distinzione accademica. L’uso canonico di ^~ è tenere una directory scrivibile lontana da un interprete:

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

Togli ^~ e una richiesta per /uploads/evil.php finisce dritta a PHP-FPM. È la forma all’origine di una lunga serie di segnalazioni di upload-to-RCE, e la differenza tra vulnerabile e sicuro sono due caratteri. È anche il motivo per cui il caso del “^~ sconfitto” conta: aggiungere un prefisso semplice più lungo come location /uploads/thumbs/ riapre il buco per tutto quello che ci sta sotto, e il diff che lo fa sembra del tutto innocuo.

Occhio anche all’ambito. ^~ sopprime solo le regex dichiarate al proprio livello; non sopprime mai le regex annidate dentro il suo stesso blocco, e un ^~ su una location annidata non può proteggere da una regex dichiarata a livello server. Attiva e disattiva il modificatore nel tester nginx location e guarda cambiare il vincitore: le regex saltate restano in tabella, marcate come tali, invece di sparire.

Location regex: l’ordine batte la specificità

Una location regex in nginx usa ~ per la corrispondenza sensibile alle maiuscole e ~* per quella che le ignora. La corrispondenza per prefisso, invece, su Linux distingue sempre maiuscole e minuscole. (Sui filesystem che non le distinguono, come quello di macOS, nginx confronta i prefissi ignorando le maiuscole e costringe ogni location regex a comportarsi come ~*. Se sviluppi su un Mac e rilasci su Linux, quella differenza può nascondere una regola rotta fino alla messa in produzione.)

La regola che frega tutti è questa: le regex vengono valutate nell’ordine in cui compaiono nel file di configurazione, e la prima corrispondenza chiude la ricerca. Specificità, lunghezza e ancoraggi non hanno alcuna influenza sull’ordine.

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

Una richiesta per /a/b/c se la prende ~ ^/a. Il secondo blocco corrisponde esattamente all’URI, è molto più preciso e non verrà mai eseguito per nessuna richiesta. È configurazione morta che nginx -t accetta senza dire una parola.

L’abitudine da prendere, quindi, è ordinare le regex dalla più specifica alla più generica e tenere la lista corta. Un pattern ampio vicino all’inizio rende irraggiungibile tutto quello che sta sotto, e siccome un diff che si limita a riordinare delle righe sembra innocuo, la regressione tende ad arrivare durante una pulizia più che durante una feature.

Gli ancoraggi meritano la stessa attenzione. location ~ /admin non è ancorato a nessuna delle due estremità, quindi cerca in qualunque punto dell’URI e corrisponde tranquillamente a /public/admin/x. Scrivi ~ ^/admin quando intendi l’inizio. Ancorare solo alla fine, come in ~ \.php$, è normale e corretto per il routing per estensione.

Qualche dettaglio di PCRE che smentisce le intuizioni prese da JavaScript:

  • nginx compila i pattern delle location con PCRE, senza modalità UTF né multiline, quindi i pattern lavorano sui byte e ^ si ancora solo all’inizio dell’URI.
  • Il $ di PCRE corrisponde anche subito prima di un newline finale. Un URI che finisce in %0A soddisfa comunque \.php$, ed è un modo noto per scivolare oltre le regole basate sull’estensione del file.
  • In PCRE sono comuni costrutti senza equivalente in JavaScript: gruppi atomici (?>…), quantificatori possessivi a*+, modificatori inline come (?i), classi POSIX come [[:alpha:]] ed escape come \A, \z, \K e \Q…\E.
  • Un pattern che contiene { o } va messo tra virgolette. location ~ ^/a{2}$ non si carica e restituisce unknown directive "2}$", perché la graffa ha chiuso il token. Scrivi location ~ "^/a{2}$".

I gruppi di cattura funzionano come speri, e da $1 in poi sono disponibili dentro il blocco:

upstream backend {
    server 127.0.0.1:8080;
}

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

Se stai ancora facendo il debug del pattern in sé e non della sua posizione nel file, provalo prima nel tester regex; il cheat sheet regex copre la sintassi nel dettaglio.

Il passaggio che tutti saltano: la normalizzazione dell’URI

Prima di consultare qualunque location, nginx riscrive il target della richiesta. I tuoi pattern vengono confrontati con il percorso normalizzato, non con i byte arrivati sul filo. Il dettaglio compare di rado nei tutorial, e decide un numero sorprendente di casi “la mia location non corrisponde”.

La normalizzazione fa quattro cose: stacca la query string, applica la decodifica percent al percorso, risolve i segmenti . e .. e comprime gli slash ripetuti.

Target della richiestaValore normalizzato di $uriNota
//a//x/a/xSlash ripetuti uniti
/a/../b/x/b/x.. risolto prima della corrispondenza
/a/b%2F..%2Fzz/a/zz%2F si decodifica in un separatore vero e entra nella risoluzione
/a/%2e%2e/b/x/b/x%2E si decodifica in un punto che partecipa anche lui
/a%20b/x/a b/x%20 diventa uno spazio vero
/a+b/x/a+b/xIn un percorso + non è uno spazio
/a?x=/b/aLa query string viene staccata per prima
/a%3Fx=1/a?x=1%3F resta letterale; la query string è vuota

Tre caratteri decodificati fanno eccezione e vengono riscritti alla lettera senza essere reinterpretati: %25, %23 e %3F. È per questo che /a%3Fx=1 finisce con un punto interrogativo dentro il percorso e niente in $args.

Due tipi di target non arrivano mai alla selezione della location. I segmenti .. che risalgono oltre la radice e un escape non valido come %00 vengono entrambi rifiutati con 400 prima che la corrispondenza cominci.

Da qui la conseguenza per la sicurezza. Se usi un blocco location come confine per il controllo degli accessi, il percorso che hai scritto viene confrontato con il percorso risolto:

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

Una richiesta per /a/b%2F..%2Fzz non resta sotto /a/b/. Si normalizza in /a/zz e viene gestita da location /a/. Ragionare sul target grezzo invece che su $uri qui porta alla risposta sbagliata, e “risposta sbagliata” in un contesto di controllo degli accessi ha un nome preciso. Prima di affidarti a location /admin per proteggere qualcosa, verifica qual è davvero il percorso normalizzato: il tester nginx location mostra il target grezzo, il valore normalizzato di $uri e la query string staccata come tre righe distinte. Se ti serve solo ragionare sulla codifica in sé, il codificatore e decodificatore URL se ne occupa a parte.

Un’ultima conseguenza: la query string non partecipa mai alla corrispondenza. location /search?q= non può corrispondere a una richiesta per /search?q=1, perché la selezione vede sempre e solo /search. Per diramare su un parametro, leggi $arg_name dentro il blocco.

Cinque configurazioni che non fanno quello che pensi

Un blocco ^~ battuto da un prefisso semplice più lungo

Sintomo: una directory ^~ sembra protetta, e una regex gestisce comunque le richieste al suo interno. Causa: ^~ viene consultato solo sul prefisso che ha già vinto sulla lunghezza. Al suo posto viene ricordato un prefisso semplice più lungo, che non sopprime nulla. Soluzione: metti ^~ anche sul prefisso più lungo, oppure elimina il prefisso più lungo.

# 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$  { }

Un prefisso senza slash finale che si prende anche i vicini

Sintomo: un blocco pensato per una sola directory serve anche percorsi che si limitano a cominciare con le stesse lettere. Causa: la corrispondenza per prefisso confronta caratteri, non segmenti di percorso, quindi location /app corrisponde anche a /application. Soluzione: scrivi lo slash finale e aggiungi location = /app se serve gestire anche il percorso nudo.

La regex specifica messa sotto quella generica

Sintomo: una regola precisa non scatta mai, e non compare nessun errore da nessuna parte. Causa: le regex vengono provate nell’ordine del file e la prima corrispondenza chiude la ricerca, quindi tutto ciò che sta sotto un pattern generico è irraggiungibile. Soluzione: sposta il pattern specifico sopra quello generico, oppure stringi quello generico con un ancoraggio.

Una regex scritta dopo ^~

Sintomo: una regola deny si carica senza problemi e non blocca niente. Causa: ^~ accetta un prefisso letterale, non un pattern. nginx non protesta mai; semplicemente il blocco non corrisponde a nessun URI. Soluzione: usa il modificatore regex.

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

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

L’idea che la query string partecipi

Sintomo: una location che contiene ? non corrisponde mai. Causa: la query string viene staccata durante la normalizzazione e la selezione della location gira solo sul percorso. Soluzione: fai corrispondere il percorso e leggi $arg_name dentro il blocco.

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

Debug: scopri quale blocco ha vinto davvero

Il log di debug è la risposta autorevole, ed è anche quella che costa più fatica da preparare. Richiede un binario compilato con il supporto per il debug, quindi verifica prima:

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

Poi abilitalo e cerca con grep la riga che nomina il blocco selezionato:

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

Le sonde sugli header di risposta sono più rapide e non richiedono una build di debug. Etichetta ogni candidato e rileggi gli header:

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

--path-as-is conta: senza quel flag curl risolve .. per te, con le migliori intenzioni, e finisci per testare un URI diverso da quello che avevi in mente. Se stai mettendo insieme qualcosa di più elaborato, il generatore di comandi cURL scrive i flag al posto tuo, e il cheat sheet curl copre il resto. Quando la sonda torna con un redirect o un 404 invece che con il tuo header, di solito il riferimento sui codici HTTP ti dice quale modulo l’ha prodotto.

nginx -T stampa la configurazione completamente unita, file include compresi. È così che scopri in che ordine stanno davvero le tue regex una volta che i file sono stati assemblati, e raramente coincide con l’ordine del file che stavi modificando.

nginx -T | grep -n "location"

Riproduci il problema nel tester nginx location prima di mettere mano al server. Iterare su una configurazione che non hai ancora rilasciato è più veloce di un ciclo di reload, e la tabella delle decisioni dice a che punto ogni blocco è stato eliminato.

Location annidate, try_files e cosa non cambiano

Le location annidate eseguono lo stesso algoritmo un livello più sotto. Quando una location con prefisso vince, nginx scende nei suoi figli e ripete lì la ricerca, il che significa che una regex annidata viene provata prima delle regex del livello padre:

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

/a/x.php viene gestito dal blocco annidato. L’annidamento ha anche un effetto meno ovvio: può rendere irraggiungibile un prefisso globalmente più lungo, perché si scende solo nel vincitore di ciascun livello. Se location /a/bb/ è annidata dentro location /a/, e al livello esterno c’è una vicina location /a/b, una richiesta per /a/bb/x finisce a /a/b. Il confronto esterno avviene per primo, e lo vince /a/b. E una discesa che non trova nulla non torna indietro: la richiesta resta al padre.

try_files e rewrite non fanno parte della selezione. Vengono eseguiti dentro il blocco che ha già vinto e non possono tornare indietro a cambiarlo. Se una richiesta non arriva mai al blocco che contiene il tuo try_files, la direttiva è irrilevante, e il colpevole abituale è una regex ~ \.php$ che si prende la richiesta prima che il blocco con prefisso abbia il suo turno. L’unica eccezione che vale la pena conoscere: un redirect interno (rewrite … last, oppure un salto via error_page) fa ripartire la corrispondenza, quindi l’URI riscritto viene risolto di nuovo sull’elenco delle location partendo dall’alto.

Infine, il 301 che ricevi quando chiedi una directory senza slash finale non è un errore di corrispondenza. A produrlo sono due meccanismi distinti. Se una location il cui nome finisce con / porta proxy_pass o un’altra direttiva *_pass, una richiesta per lo stesso percorso senza slash riceve un 301 durante la selezione, prima che venga valutata qualunque regex e con la query string conservata:

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

Aggiungere location = /user elimina quel redirect. Il modulo dei file statici, invece, emette un 301 tutto suo quando un percorso si risolve in una directory reale su disco: lì decide il filesystem, non la tua configurazione.

FAQ

Quali sono i cinque modificatori delle location in nginx?

I cinque modificatori delle location in nginx sono = per la corrispondenza esatta, ^~ per un prefisso che salta la fase regex, nessun modificatore per un prefisso ordinario, ~ per una regex sensibile alle maiuscole e ~* per una che le ignora. Una sesta forma, location @name, non partecipa mai alla corrispondenza degli URI ed esiste solo come destinazione per error_page e try_files.

Una corrispondenza esatta = rende nginx più veloce?

Una location con corrispondenza esatta chiude subito la ricerca, saltando la scansione dei prefissi e ogni valutazione di regex. Il risparmio è reale ma troppo piccolo per accorgersene. Vale la pena scriverla per endpoint colpiti migliaia di volte al secondo, come gli health check o /favicon.ico. Una pila di blocchi = per pagine ordinarie costa più in complessità di configurazione di quanto renda.

Posso scrivere un modificatore di location senza spazio, tipo ~*^/api?

Sì. location ~*^/api/ e location ~* ^/api/ significano esattamente la stessa cosa, perché nginx stacca il modificatore dall’inizio del nome e riconosce per primo il modificatore più lungo, quindi ~* viene riconosciuto prima di ~. Lo spazio mettilo lo stesso: un modificatore attaccato sembra parte del pattern, e in review chi legge lo interpreta male.

Qual è la differenza tra root e alias dentro un blocco location?

root accoda l’intero URI alla directory, mentre alias sostituisce con essa il prefisso che ha corrisposto. Con location /static/ { root /var/www; } una richiesta per /static/x.css cerca /var/www/static/x.css; passa a alias /var/www/assets/; e cerca /var/www/assets/x.css. Con alias, o metti lo slash finale sia alla location sia al percorso, oppure non mettilo a nessuno dei due.

Una richiesta può corrispondere a più di un blocco location?

Possono corrispondere più blocchi, ma a gestire la richiesta ne resta esattamente uno. nginx confronta ogni location con prefisso e, se serve, ogni location regex, poi consegna la richiesta all’unico vincitore. Dai blocchi perdenti non si eredita nessuna direttiva: quello che ti serve ovunque deve stare a livello server o http, oppure va ripetuto.

nginx -t mi dice quale location corrisponderà?

No. nginx -t verifica la sintassi e la validità della configurazione; non simula mai una richiesta, quindi sull’ordine di corrispondenza non dice nulla. Per scoprire a chi appartiene un URI, leggi il log di debug, aggiungi un header di risposta temporaneo oppure incolla la configurazione nel tester nginx location e leggi perché ogni blocco ha vinto o perso.

Tag: nginx web-server devops regex configuration

Articoli correlati

Vedi tutti gli articoli