Każda część adresu URL i to, co może zawierać

Każda część adresu URL opisana na konkretnym przykładzie, które znaki wymagają kodowania procentowego i w którym miejscu, jak naprawdę zachowują się ciągi zapytania oraz jak rozpoznać odnośnik, który kłamie na temat swojego hosta.

Adres URL to jeden ciąg znaków, który mówi, jakim protokołem się posłużyć, z którą maszyną rozmawiać i o co poprosić. Poprawne odczytanie go sprowadza się głównie do wiedzy, gdzie kończy się każda część, ponieważ separatorami są pojedyncze znaki, a liczy się pierwszy napotkany.

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

Poszczególne części

CzęśćOgraniczona przezDo czego służyTrafia na serwer
Schematkończy się na pierwszym :Który protokół i które reguły obowiązują dla resztyDecyduje o połączeniu
Userinfopo //, kończy się na @Dane logowania, od dawna wycofane dla http i httpsTylko jeśli klient zdecyduje się je wysłać
Hostkończy się na :, /, ? lub #Nazwa domeny albo adres IP, z którym trzeba się połączyćTak, w nagłówku Host
Portpo :, kończy się na /, ? lub #Port TCP, domyślnie 80 dla http i 443 dla httpsUżywany przy nawiązywaniu połączenia
Ścieżkazaczyna się od /, kończy się na ? lub #Który zasób na tym hościeTak, w wierszu żądania
Zapytaniezaczyna się od ?, kończy się na #Parametry dla tego zasobuTak, w wierszu żądania
Fragmentzaczyna się od #, biegnie do końcaMiejsce wewnątrz zasobuNie

Host i port razem tworzą authority. Fragment jest tu wyjątkiem: zostaje usunięty, zanim żądanie zostanie zbudowane, więc serwer nigdy go nie widzi. Przeglądarki używają go do kotwic i do routingu po stronie klienta, a niektóre procesy uwierzytelniania celowo zwracają tokeny we fragmencie, żeby te tokeny nigdy nie pojawiły się w logu serwera. Fragment i tak trafia do historii przeglądarki, więc nie jest prywatny, jest tylko niewysyłany.

Znaki zarezerwowane i niezarezerwowane

Kodowanie procentowe zapisuje bajt jako % plus dwie cyfry szesnastkowe, korzystając z bajtów UTF-8 danego znaku. é staje się %C3%A9, dwa bajty i dwie sekwencje ucieczki.

Cztery znaki są niezarezerwowane obok liter i cyfr i nigdzie nie wymagają kodowania: - . _ ~. Cała reszta jest albo zarezerwowana, czyli pełni gdzieś w adresie URL rolę strukturalną, albo musi zostać zakodowana. Zbiór zarezerwowany to : / ? # [ ] @ oraz ! $ & ' ( ) * + , ; =.

Słowo "zarezerwowany" nie oznacza "zawsze zabroniony". Oznacza, że znak jest separatorem w którejś części i musi zostać zakodowany tylko tam, gdzie zostałby odczytany jako separator.

ZnakW segmencie ścieżkiW wartości zapytania
Spacja%20, zawsze%20 albo +
/%2F, inaczej rozdziela segmentDozwolony w tej postaci
?%3F, inaczej ścieżka się tam kończyDozwolony w tej postaci
#%23, zawsze%23, zawsze
& i =Dozwolone w tej postaci%26 i %3D, inaczej para się rozpada
+Dozwolony i oznacza plus%2B, inaczej może zostać zdekodowany jako spacja

Praktyczna reguła brzmi więc tak: koduj ten znak, który zakończyłby część, w której właśnie jesteś. Spacja w ścieżce musi być zapisana jako %20, bo surowa spacja kończy adres URL w większości parserów, natomiast spacja w wartości zapytania może być zapisana jako %20 albo starszym +. Koder URL to pewny sposób, żeby to zrobić, ponieważ zakodowanie całego adresu i zakodowanie pojedynczego komponentu to dwie różne operacje, a prawie zawsze chodzi o tę drugą.

Ciąg zapytania to tylko konwencja

Specyfikacja mówi wyłącznie tyle, że zapytanie to wszystko pomiędzy ? a #, złożone z dozwolonych znaków. Postać key=value&key=value pochodzi z kodowania formularzy HTML, a nie ze standardu URI. Nic nie stoi na przeszkodzie, żeby serwer parsował ?a:1;b:2 tak, jak mu się podoba.

Ponieważ ta postać jest tylko konwencją, powtórzone klucze nie mają zdefiniowanego znaczenia i każdy stos technologiczny wybrał własną odpowiedź:

Zachowanie dla ?id=1&id=2Gdzie
Obie wartości, jako listaPython parse_qs, Node querystring, Express
Tylko pierwsza wartośćGo Query().Get, Java getParameter
Tylko ostatnia wartośćPHP, Rails
Sklejone w 1,2kolekcje żądań w ASP.NET

Tablice dziedziczą ten sam problem. ids=1&ids=2, ids[]=1&ids[]=2, ids[0]=1&ids[1]=2 oraz ids=1,2 są wszystkie szeroko używane i tylko serwer decyduje, którą postać rozumie. Zapis z nawiasami trzeba zakodować, żeby był ściśle zgodny z normą (ids%5B%5D=1), choć większość serwerów akceptuje surowe nawiasy. Wybierz tę postać, którą dokumentuje strona odbierająca, a potem trzymaj się jej konsekwentnie.

Znak plus zasługuje na osobne ostrzeżenie. W danych application/x-www-form-urlencoded, czyli w treści żądania zakodowanej jak formularz, + oznacza spację. W ścieżce URL oznacza dosłowny plus. W ciągu zapytania zależy to od parsera, a większość frameworków webowych stosuje tam dekodowanie formularzowe, więc + po cichu zamienia się w spację. Każda wartość, która może w uzasadniony sposób zawierać plus, na przykład standardowe wyjście Base64 albo numer telefonu, musi zostać wysłana jako %2B.

Odwołania względne

Odwołanie względne jest rozwiązywane wobec adresu bazowego przez zastąpienie wszystkiego, co znajduje się po ostatnim / w tym adresie. Ten ostatni ukośnik to cała reguła i właśnie dlatego ukośnik na końcu ma znaczenie.

Adres bazowyOdwołanieWynik
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

Ostatni wiersz to odwołanie względne wobec schematu: dwa wiodące ukośniki zachowują schemat adresu bazowego i zastępują authority. Odwołanie zaczynające się od ? zachowuje ścieżkę i porzuca stare zapytanie, a takie, które zaczyna się od #, zachowuje jedno i drugie.

Kiedy dwa adresy URL to ten sam zasób

Schemat i host nie rozróżniają wielkości liter, więc HTTPS://Example.COM i https://example.com to jeden adres. Wszystko po hoście jest, z punktu widzenia standardu, wrażliwe na wielkość liter, nawet jeśli konkretny serwer postanawia ją ignorować.

Te pary są równoważne:

  • https://example.com:443/a i https://example.com/a, ponieważ port jest domyślny
  • https://example.com i https://example.com/, ponieważ pusta ścieżka oznacza katalog główny
  • /a/%7Euser i /a/~user, ponieważ ~ jest niezarezerwowany i zakodowanie go niczego nie zmienia

Te pary równoważne nie są:

  • /docs i /docs/, które są różnymi zasobami, choć większość serwerów przekierowuje jeden na drugi
  • /p?a=1&b=2 i /p?b=2&a=1, ponieważ kolejność parametrów jest częścią ciągu znaków
  • /index.html i /, chyba że serwer mówi inaczej

Pamięci podręczne i sieci CDN kluczują po dokładnych bajtach, więc dodany parametr śledzący albo przestawiona kolejność zapytania rozbija jeden obiekt w pamięci podręcznej na dwa i o połowę obniża trafienia. Wyszukiwarki traktują warianty jako zduplikowane strony, dopóki odnośnik kanoniczny nie wskaże jednej wybranej postaci. Rozwiązaniem jest wybrać jedną postać, przekierować na nią resztę kodem 301 i usuwać parametry, które nie zmieniają odpowiedzi.

Długość, logi i prywatność

W specyfikacji nie ma limitu długości, ale limity istnieją wszędzie indziej. Typowe domyślne ustawienia serwerów ograniczają cały wiersz żądania do około 8 KB, niektóre serwery proxy i urządzenia sieciowe do 4 KB, a starsze klienty do mniej więcej 2 KB. Wszystko, co ma przetrwać programy pocztowe, kody QR i skracacze linków, jest najbezpieczniejsze poniżej mniej więcej 2000 znaków.

Mocniejszym argumentem za krótkimi adresami URL jest to, że ciąg zapytania nie jest prywatny. Trafia do logów dostępu, jest przekazywany podmiotom trzecim w nagłówku Referer, zostaje w historii przeglądarki, zapisuje się razem z zakładką i jest kopiowany za każdym razem, gdy ktoś udostępnia odnośnik. Tokeny sesji, kody resetu hasła, klucze API i dane osobowe nie mają tam więc czego szukać. Dane, które są duże albo wrażliwe, idą do treści żądania, która nie ma praktycznego limitu rozmiaru i domyślnie nie jest logowana.

Czytanie odnośnika przed kliknięciem

Authority zaczyna się po :// i kończy na pierwszym napotkanym /, ? lub #. Odczytaj ten odcinek, a potem odczytaj jego dwie albo trzy ostatnie etykiety. To jest prawdziwy host i nic, co stoi po jego lewej stronie, tego nie zmienia.

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

Typowe przebrania, które warto rozpoznawać:

  • Tekst przed @ to userinfo, nigdy host, i może to być dowolna nazwa marki.
  • paypal.com.secure-login.example to subdomena secure-login.example. Etykiety czyta się od prawej do lewej.
  • https://short.example/https://www.bank.com/login umieszcza cały przekonująco wyglądający adres URL w ścieżce.
  • Host zaczynający się od xn-- to Punycode, czyli postać ASCII umiędzynarodowionej nazwy domeny wytworzona przez IDNA. münchen.de to w transmisji xn--mnchen-3ya.de, co jest zupełnie legalne, ale ten sam mechanizm umożliwia homografy: cyrylickie а (U+0430) wygląda identycznie jak łacińskie a, a domena, która go używa, koduje się do czegoś w rodzaju xn--pple-43d.com. Przeglądarki pokazują postać Punycode, gdy etykieta miesza pisma, choć reguły bywają różne i nie dają gwarancji.
  • Zakodowane separatory takie jak %2F czy %40 wewnątrz hosta to celowa próba zmylenia parsera.

Wklejenie odnośnika do parsera URL rozstrzyga sprawę w jednym kroku, bo prawdziwy parser stosuje tę samą regułę pierwszego separatora, którą zastosuje przeglądarka, i pokazuje host osobno. Do sprawdzania wielu odnośników naraz tester wyrażeń regularnych jest szybkim sposobem na potwierdzenie, które z nich rzeczywiście mają oczekiwane authority.