Ogni parte di un URL e cosa può contenere

Ogni parte di un URL etichettata su un esempio concreto, quali caratteri vanno codificati in percentuale e dove, come si comportano davvero le query string e come riconoscere un link che mente sul proprio host.

Un URL è una singola stringa che dice quale protocollo parlare, con quale macchina parlarlo e cosa chiedere. Leggerlo correttamente è soprattutto una questione di sapere dove finisce ciascuna parte, perché i delimitatori sono caratteri singoli e vince il primo.

https://alex:s3cret@www.example.co.uk:8443/docs/intro?lang=en&page=2#notes
│       │           │                 │   │           │              └─ fragment
│       │           │                 │   │           └──────────────── query
│       │           │                 │   └──────────────────────────── path
│       │           │                 └──────────────────────────────── port
│       │           └────────────────────────────────────────────────── host
│       └────────────────────────────────────────────────────────────── userinfo
└────────────────────────────────────────────────────────────────────── scheme

Le parti

ParteDelimitata daA cosa serveArriva al server
Schemafinisce al primo :Quale protocollo e quali regole valgono per il restoDetermina la connessione
Userinfodopo //, finisce a @Credenziali, deprecate da tempo per http e httpsSolo se il client sceglie di inviarle
Hostfinisce a :, /, ? o #Un nome di dominio o un indirizzo IP da risolvere e a cui connettersiSì, nell'header Host
Portadopo :, finisce a /, ? o #La porta TCP, che in assenza vale 80 per http e 443 per httpsUsata per la connessione
Percorsoinizia a /, finisce a ? o #Quale risorsa su quell'hostSì, nella riga di richiesta
Queryinizia a ?, finisce a #Parametri per quella risorsaSì, nella riga di richiesta
Frammentoinizia a #, arriva fino alla fineUna posizione all'interno della risorsaNo

Host e porta insieme formano l'autorità. Il frammento è l'eccezione: viene rimosso prima che la richiesta sia costruita, quindi un server non lo vede mai. I browser lo usano per le ancore e per il routing lato client, e alcuni flussi di autenticazione restituiscono deliberatamente i token in un frammento, così quei token non compaiono mai in un log del server. Finisce comunque nella cronologia del browser, quindi non è privato, solo non inviato.

Caratteri riservati e non riservati

La codifica percentuale scrive un byte come % seguito da due cifre esadecimali, usando i byte UTF-8 del carattere. é diventa %C3%A9, due byte e due sequenze di escape.

Quattro caratteri sono non riservati accanto a lettere e cifre, e non vanno mai codificati da nessuna parte: - . _ ~. Tutto il resto è riservato, cioè ha un compito strutturale da qualche parte in un URL, oppure va codificato. L'insieme riservato è : / ? # [ ] @ e ! $ & ' ( ) * + , ; =.

La parola «riservato» non significa «sempre vietato». Significa che il carattere è un delimitatore in qualche parte, e va codificato solo nelle parti in cui verrebbe letto come tale.

CarattereIn un segmento di percorsoIn un valore di query
Spazio%20, sempre%20 oppure +
/%2F, altrimenti divide il segmentoValido così com'è
?%3F, altrimenti il percorso finisce lìValido così com'è
#%23, sempre%23, sempre
& e =Validi così come sono%26 e %3D, altrimenti la coppia si spezza
+Valido, e significa un più%2B, altrimenti può essere decodificato come uno spazio

La regola pratica è questa: codifica il carattere che chiuderebbe la parte in cui ti trovi. Uno spazio in un percorso deve essere %20, perché nella maggior parte dei parser uno spazio grezzo chiude l'URL, mentre uno spazio in un valore di query può essere %20 o il più vecchio +. Un Codificatore URL è il modo affidabile per farlo, dato che codificare un URL intero e codificare un singolo componente sono operazioni diverse, e quasi sempre serve la seconda.

Le query string sono una convenzione

La specifica dice soltanto che la query è tutto ciò che sta fra ? e #, scelto fra i caratteri ammessi. La forma key=value&key=value viene dalla codifica dei form HTML, non dallo standard URI. Nulla impedisce a un server di interpretare ?a:1;b:2 come preferisce.

Poiché la forma è una convenzione, le chiavi ripetute non hanno un significato definito, e ogni stack ha scelto la propria risposta:

Comportamento con ?id=1&id=2Dove
Entrambi i valori, come listaPython parse_qs, Node querystring, Express
Solo il primo valoreGo Query().Get, Java getParameter
Solo l'ultimo valorePHP, Rails
Uniti in 1,2Le request collection di ASP.NET

Gli array ereditano lo stesso problema. ids=1&ids=2, ids[]=1&ids[]=2, ids[0]=1&ids[1]=2 e ids=1,2 sono tutti molto diffusi, e solo il server decide quale di essi capisce. La notazione con parentesi quadre va codificata per essere rigorosamente valida (ids%5B%5D=1), anche se la maggior parte dei server accetta le parentesi grezze. Scegli la forma documentata da chi riceve, poi mantienila coerente.

Il segno più merita un avviso a parte. Nei dati application/x-www-form-urlencoded, cioè un corpo di richiesta codificato come un form, + significa spazio. In un percorso URL significa un più letterale. In una query string dipende dal parser, e la maggior parte dei framework web vi applica la decodifica dei form, quindi + diventa silenziosamente uno spazio. Qualsiasi valore che possa legittimamente contenere un più, come l'output Base64 standard o un numero di telefono, va inviato come %2B.

Riferimenti relativi

Un riferimento relativo si risolve rispetto a un URL di base sostituendo tutto ciò che segue l'ultima / della base. Quell'ultima barra è tutta la regola, ed è il motivo per cui la barra finale conta.

BaseRiferimentoRisultato
https://ex.com/docs/introguidehttps://ex.com/docs/guide
https://ex.com/docs/intro/guidehttps://ex.com/docs/intro/guide
https://ex.com/docs/intro/guidehttps://ex.com/guide
https://ex.com/docs/intro/../guidehttps://ex.com/docs/guide
https://ex.com/docs/intro?a=1?b=2https://ex.com/docs/intro?b=2
https://ex.com/docs/intro?a=1#tophttps://ex.com/docs/intro?a=1#top
https://ex.com/docs/intro//cdn.ex.com/x.jshttps://cdn.ex.com/x.js

L'ultima riga è un riferimento relativo allo schema: due barre iniziali mantengono lo schema della base e sostituiscono l'autorità. Un riferimento che inizia con ? mantiene il percorso e scarta la vecchia query, mentre uno che inizia con # li mantiene entrambi.

Quando due URL sono la stessa risorsa

Schema e host non distinguono fra maiuscole e minuscole, quindi HTTPS://Example.COM e https://example.com sono un solo indirizzo. Tutto ciò che segue l'host distingue fra maiuscole e minuscole per quanto riguarda lo standard, anche se un server specifico può decidere di ignorare la differenza.

Queste coppie sono equivalenti:

  • https://example.com:443/a e https://example.com/a, perché la porta è quella predefinita
  • https://example.com e https://example.com/, perché un percorso vuoto significa la radice
  • /a/%7Euser e /a/~user, perché ~ non è riservato e codificarlo in percentuale non cambia nulla

Queste invece non lo sono:

  • /docs e /docs/, che sono risorse diverse, anche se la maggior parte dei server reindirizza l'una all'altra
  • /p?a=1&b=2 e /p?b=2&a=1, dato che l'ordine dei parametri fa parte della stringa
  • /index.html e /, a meno che il server non dica il contrario

Le cache e le CDN si basano sui byte esatti, quindi un parametro di tracciamento aggiunto o una query riordinata spezza un oggetto in cache in due e dimezza il tasso di hit. I motori di ricerca trattano le varianti come pagine duplicate, a meno che un link canonical non punti a una forma scelta. La soluzione è scegliere una forma unica, reindirizzare le altre con un 301 e togliere i parametri che non cambiano la risposta.

Lunghezza, log e privacy

Nella specifica non esiste alcun limite di lunghezza, ma i limiti esistono ovunque altrove. Le impostazioni predefinite più comuni dei server fermano l'intera riga di richiesta intorno agli 8 KB, alcuni proxy e apparati a 4 KB e i client più vecchi intorno ai 2 KB. Tutto ciò che deve sopravvivere a client di posta, codici QR e accorciatori di link sta più al sicuro sotto i 2000 caratteri circa.

L'argomento più forte per tenere corti gli URL è che una query string non è privata. Viene scritta nei log di accesso, passata a terze parti nell'header Referer, conservata nella cronologia del browser, salvata con i segnalibri e copiata ogni volta che qualcuno condivide il link. Token di sessione, codici di reimpostazione della password, chiavi API e dati personali non vanno quindi messi lì. I dati voluminosi o sensibili vanno nel corpo della richiesta, che non ha un tetto pratico di dimensione e per impostazione predefinita non finisce nei log.

L'autorità inizia dopo :// e finisce al primissimo /, ? o #. Leggi quel tratto, poi leggi le sue ultime due o tre etichette. Quello è l'host reale, e niente di ciò che sta alla sua sinistra lo cambia.

https://www.paypal.com@198.51.100.7/secure/login
        ^^^^^^^^^^^^^^ ^^^^^^^^^^^^
        userinfo       real host

Travestimenti comuni da riconoscere:

  • Il testo che precede una @ è userinfo, mai l'host, e può essere il nome di qualunque marchio.
  • paypal.com.secure-login.example è un sottodominio di secure-login.example. Le etichette si leggono da destra a sinistra.
  • https://short.example/https://www.bank.com/login mette un intero URL convincente dentro il percorso.
  • Un host che inizia con xn-- è in Punycode, la forma ASCII di un nome di dominio internazionalizzato prodotta da IDNA. münchen.de sulla rete è xn--mnchen-3ya.de, il che è legittimo, ma lo stesso meccanismo rende possibili gli omografi: la а cirillica (U+0430) è visivamente identica alla a latina, e un dominio che la usa si codifica in qualcosa come xn--pple-43d.com. I browser mostrano la forma Punycode quando un'etichetta mescola più alfabeti, anche se le regole variano e non sono una garanzia.
  • I delimitatori codificati come %2F o %40 all'interno di un host sono un tentativo deliberato di confondere un parser.

Incollare il link in un Analizzatore di URL chiude la questione in un passaggio, perché un vero parser applica la stessa regola del primo delimitatore che vince che applicherà il browser e mostra l'host per conto suo. Per controllare molti link in una volta, un Tester di regex è un modo rapido per confermare quali di essi abbiano davvero l'autorità che ti aspetti.