Inspektor kodowania procentowego

Kodowanie procentowe to nie jedna reguła, lecz kilka, które nie zgadzają się co do kilkunastu znaków. Ta strona przepuszcza ten sam tekst przez encodeURIComponent, encodeURI i serializację application/x-www-form-urlencoded obok siebie, zaznacza znaki, co do których się rozchodzą, wypisuje wiersz na każdy znak z jego bajtami UTF-8 i klasą RFC 3986 oraz mówi, ile razy ciąg został zakodowany. Strona dekodująca odczytuje ciąg według reguły ścisłej albo luźnej i wskazuje, który escape jest uszkodzony i gdzie, zamiast zwracać po cichu błędny wynik.
Całe przetwarzanie odbywa się lokalnie w przeglądarce — dane nigdy nie są wysyłane na żaden serwer.

Nie wiadomo, czym jest kodowanie procentowe? Kodowanie procentowe wyjaśnione zwykłym językiem — czym jest, dlaczego spacja staje się %20 albo +, i co oznacza, gdy adres URL pokazuje %25.
Co to robi:
Jak czytać escape'y:
encodeURIComponent
encodeURI
formularz (application/x-www-form-urlencoded)
Ctrl+Enter w polu wejściowym uruchamia aktywny tryb.
Wybierz tryb i kliknij jego przycisk akcji. Koduj i sprawdź pokazuje trzy kodowania tekstu oraz wiersz na każdy znak; Dekoduj odczytuje escape'y z powrotem i liczy warstwy. Przycisk Autotest uruchamia ponownie każdy udokumentowany przykład, każdy drukowalny znak ASCII i łańcuchy warstw zapisane w pliku danych tej strony, i podaje tutaj ich liczbę.
Gdzie kodery się nie zgadzają:tylko znaki poniżej są traktowane inaczej przez trzy metody. Cała reszta przechodzi przez wszystkie trzy bez zmian albo jest kodowana identycznie przez wszystkie trzy.
Każdy znak, po jednym wierszu:bajty UTF-8, w jakie zamienia się znak, klasa, jaką przypisuje mu RFC 3986, i escape, jaki zapisuje każdy kontekst. Cztery kolumny kontekstu to zbiory kodowania procentowego WHATWG, używane przez przeglądarkę przy budowaniu ścieżki, zapytania lub fragmentu; zacieniony wiersz to znak, który trzy kodery traktują inaczej.
Dokładny zbiór, który ta strona koduje dla każdej części URL:wypisane z samego kodera, więc ta tabela i kod wypełniający kolumny powyżej nie mogą się rozjechać. Każdy zbiór koduje też znaki sterujące C0 (U+0000 do U+001F) i każdy punkt kodowy powyżej U+007E, dlatego tekst spoza ASCII jest zawsze kodowany w każdym kontekście.
Te zbiory są wyprowadzone ze zbiorów kodowania procentowego WHATWG i różnią się od nich w dwóch świadomych miejscach, które oba czynią ręczne składanie bezpiecznym: krzyżyk jest kodowany wewnątrz zapytania, bo surowy zacząłby fragment; a zbiór komponentu jest przypięty dokładnie do znaków, które koduje encodeURIComponent, łącznie z apostrofem, a zbiór formularza do tego, co koduje URLSearchParams. Dwie kolumny w tabeli powyżej są więc zachowaniem samej platformy, a nie jego reimplementacją. Ta lista wyjaśnia też wynik, który ludzie uważają za zaskakujący: znak procentu jest kodowany przez encodeURIComponent i przez encodeURI, ale nie należy do żadnego zbioru kontekstu WHATWG, więc kolumny ścieżki, zapytania i fragmentu w tabeli znak po znaku zostawiają go w spokoju. Surowy znak procentu w URL złożonym ręcznie nadal jest ryzykiem, bo standard zakłada, że wejście zostało wcześniej sprawdzone: %41 wewnątrz zapytania to dla wielu frameworków serwerowych pojedyncze A, a ścisły odczyt tej strony traktuje samotny % jako uszkodzony escape. Zakodowanie go samodzielnie jako %25 to bezpieczniejszy nawyk, a tabela znak po znaku pokazuje obie pisownie obok siebie.
Ile razy ten ciąg został zakodowany:każda runda dekodowania to jedna warstwa. Wartość zakodowana dwukrotnie zaczyna się od%25gdzie pojedyncze kodowanie zostawiłoby%20, a wartość zakodowana trzy razy zaczyna się od%2525.
Znaki, które URL może zawierać bez zmian:RFC 3986 dzieli zakres ASCII na trzy grupy, a reguły escape'ów mają sens tylko na tle tego podziału.
Znaki zarezerwowane nie są zakazane, one są zajęte: coś znaczą dla parsera URL. Reguła wynikająca z listy jest krótka — jeśli znak jest zarezerwowany, a ma być danymi, zakoduj go; jeśli jest nierezerwowany, nigdy nie trzeba.
Trzy metody i to, co każda z nich zostawia w spokoju:
Listy to dokładne zbiory, które każda metoda zostawia niezakodowane w zakresie ASCII; Autotest sprawdza względem nich każdy drukowalny znak ASCII, więc tabela nie może odjechać od koderów, które opisuje.
Jak duże wejście strona przyjmuje i dlaczego:kodowanie procentowe to jednoprzebiegowe przejście przez tekst, więc w przeciwieństwie do konwerterów systemów liczbowych na tej stronie koszt rośnie liniowo, a nie kwadratowo: nie ma długości, przy której sama arytmetyka staje się problemem. Poniższe limity istnieją, bo tabela znak po znaku jest rysowana w dokumencie, a tabela z milionem wierszy to właśnie to, co realnie zatrzymuje przeglądarkę.
Czym jest kodowanie procentowe?

Kodowanie procentowe — zwane też kodowaniem URL, a w starszych dokumentach escapingiem — to reguła, która pozwala URL przenosić znaki, których nie wolno mu zawierać. Escape procentowy to trzy znaki ASCII: znak procentu i dwie cyfry szesnastkowe, oznaczające jeden bajt. %20 to bajt 0x20, czyli spacja. %2F to bajt 0x2F, czyli ukośnik. Nic poza tym nie gra roli: escape nazywa bajt, a nie znak.

Ponieważ escape nazywa bajty, tekst najpierw trzeba zamienić na bajty, a kodowaniem bajtów jest UTF-8. Stąd biorą się długie escape'y: jeden znak chiński to trzy bajty UTF-8, a więc trzy escape'y, a jedno emoji to cztery bajty i dwanaście znaków ASCII.

Wersja na 30 sekund: spacja to %20 jako wartość URL, ale + w danych formularza; # to %23; dosłowny znak procentu to %25. Trzy kodery na tej stronie zgadzają się co do liter, cyfr i -_., a nie zgadzają się co do kilkunastu znaków interpunkcyjnych, w tym tyldy — i dlatego encodeURIComponent oraz encodeURI dają różne ciągi dla tego samego tekstu, a użycie złego z nich może rozbić wartość na dwa parametry zapytania.
Dlaczego URL w ogóle tego potrzebuje

URI definiuje się jako ciąg znaków ASCII (RFC 3986, sekcja 2.1), a w tym zakresie ASCII niektóre znaki są zarezerwowane: to interpunkcja nadająca URL strukturę. Dwukropek i dwa ukośniki oddzielają schemat, znak @ oddziela dane użytkownika od hosta, znak zapytania zaczyna zapytanie, krzyżyk zaczyna fragment, ampersand oddziela parametry zapytania, a znak procentu wprowadza escape.

To tworzy jeden problem i jedną lukę. Problem: wartość, która sama zawiera jeden z tych znaków — pole wyszukiwania z "a&b", nazwa pliku z ukośnikiem, hasło z dwukropkiem — w przeciwnym razie zmieniłaby strukturę URL, w którym podróżuje. Luka: sam znak procentu, więc koder może po prostu rozszerzyć własny alfabet. % zapisuje się jako %25, a dwie cyfry szesnastkowe po nim są zwykłymi znakami nierezerwowanymi.

Przykłady

To są dokładne wyjścia trzech koderów uruchamianych przez tę stronę, dla wejść dobranych tak, by zachowywały się inaczej niż pozostałe. Strona przelicza je na bieżąco, a Autotest ponownie sprawdza każdy wiersz względem implementacji platformy.

Trzy metody i kiedy każda jest właściwa

Krótka wersja, która oszczędza większość błędów: wszystko, co trafia do pojedynczej wartości parametru, przechodzi przez encodeURIComponent (albo przez serializator formularza, który robi to samo z innym zbiorem bezpiecznym), a kompletny, już złożony URL przechodzi przez encodeURI, jeśli w ogóle wymaga kodowania. Sięgnięcie po złą z tych dwóch funkcji to najczęstszy pojedynczy błąd kodowania procentowego w kodzie webowym.

Znaki, które adres URL może zawierać, w pełni
Dwa znaki plus, dwa znaczenia

Spacja to jedyny znak, którego kodowanie zależy od tego, dokąd tekst zmierza, a winien temu jest plus.

Pułapka leży w środku tej tabeli. W danych formularza plus oznacza spację, więc wartość, która naprawdę zawiera plus — numer telefonu zapisany jako +44 20 7946 0958, wyrażenie arytmetyczne, adres e-mail z tagiem plus — musi zostać przesłana jako %2B. Jeśli zostanie przesłana jako dosłowny plus, odbiorca odczyta ją jako spację, a +44 dotrze jako " 44".

Zakodowane dwukrotnie i jak to rozpoznać

Podwójne kodowanie następuje, gdy ciąg jest już zakodowany i zostaje zakodowany ponownie. Znak procentu pierwszego escape'a jest zwykłym znakiem, więc staje się %25, a warstwa pod spodem przetrwa jako widoczny tekst.

Rozpoznanie tego to kwestia liczenia: zdekoduj ciąg raz i spójrz na wynik. Jeśli nadal zawiera escape'y, które wyglądają na zamierzone — poprawne %XX albo plus formularza tam, gdzie oczekiwana jest spacja — jest kolejna warstwa. Jeśli zawiera %25, za którym następują dwie cyfry szesnastkowe, następna warstwa jest na pewno escape'em.

Dwie praktyczne uwagi. Po pierwsze, dekodowanie dwukrotne nie jest nieszkodliwe: to dobrze znany sposób, by filtr bezpieczeństwa i kod za nim widziały różne ciągi, więc filtr działający przed drugim dekodowaniem filtruje niewłaściwy tekst. Po drugie, liczba warstw tutaj to własność ciągu, a nie obietnica o intencji: prawidłowa wartość może mieć jedną warstwę, a uszkodzona może mieć dwie.

Błędy, na które ta strona została zbudowana
  • Wartość zakodowana przez encodeURI. Separatory przetrwają, więc wartość zawierająca & albo = rozbija się po drugiej stronie na kilka parametrów. Porównaj dwie kolumny na górze tej strony dla wejścia a&b=c: kolumna komponentu koduje oba znaki, a kolumna encodeURI zostawia je.
  • Dosłowny znak plus wysłany jako dane formularza. Dociera jako spacja. Zapisz go jako %2B.
  • Wartość zakodowana dwukrotnie „dla bezpieczeństwa”. Nie jest to bezpieczniejsze. Odbiorca dekoduje raz i dostaje ciąg, który nadal zawiera escape'y procentowe — to ta sama awaria co zły zestaw znaków, o jedną warstwę niżej.
  • Dekodowanie tego, co nigdy nie zostało zakodowane. Dosłowny znak procentu w tekście — 100% sure — nie jest escape'em. Przy ścisłym odczycie jest uszkodzonym escape'em, na pozycji podanej przez tę stronę; przy luźnym odczycie przetrwa, dlatego luźny jest tutaj domyślny, a nie odwrotnie.
  • Zakładanie, że escape musi być wielką literą. Dekodowanie traktuje %2f i %2F jako ten sam bajt, a oba są poprawne. Wielka litera to tylko kanoniczna pisownia, którą zaleca RFC 3986 i którą wytwarzają inne narzędzia.
  • Zakładanie, że kodowanie procentowe czyni tekst bezpiecznym. Czyni tekst jednoznacznym wewnątrz URL. Nie zatrzymuje wstrzyknięć, nie jest szyfrowaniem i nie jest sanitizerem: kodowanie musi być ostatnim krokiem przed złożeniem, dla kontekstu, do którego tekst trafia.
Krótkie odpowiedzi

„Czy plus w adresie URL to spacja?”

Tylko w danych formularza i tylko w części zapytania zgłoszenia formularza. W ścieżce, we fragmencie i w zapytaniu zwykłego linku plus jest dosłownym plusem. Dlatego ta strona ma przełącznik dla tego, zamiast zgadywać.

„Dlaczego mój link pokazuje %25?”

Ponieważ coś zakodowało znak procentu. Albo dosłowny znak procentu przeszedł przez koder — 100% sure staje się 100%25%20sure — albo ciąg, który już zawierał escape, został zakodowany powtórnie. Zdekoduj ciąg raz i sprawdź, co wraca: jeśli wynik nadal zawiera %XX, jest jeszcze jedna warstwa.

„Czy ma znaczenie, czy ukośnik w wartości zapytania to %2F czy /?”

Dla parsera URL nie: oba docierają do serwera jako ukośnik, a zbiór zapytania WHATWG zostawia ukośnik w spokoju. Dla interoperacyjności czasem tak: wiele frameworków serwerowych wykonuje własne dekodowanie na surowym ciągu zapytania, a kilka z nich różni się w sprawie znaków, które nigdy nie zostały zakodowane. Gdy wartość jest składana ręcznie zamiast przez serializator, bezpieczniejszym nawykiem jest jawne kodowanie znaków zarezerwowanych.

„Dlaczego kolumna formularza koduje tyldę, którą pozostałe dwie zostawiają w spokoju?”

Ponieważ bezpieczny zestaw kodera formularza jest węższy: tylko litery, cyfry i -_.*. Koder formularza WHATWG koduje ~ ! ' ( ), mimo że RFC 3986 uznaje ~ za nierezerwowany. Obie pisownie dekodują się do tej samej wartości; różnica ma znaczenie tylko przy porównywaniu ciągów bajt po bajcie.

„Czy te same znaki mogą pojawić się niezakodowane w pasku adresu przeglądarki?”

Tak, i chodzi o pasek adresu, a nie o URL. Przeglądarki pokazują czytelną postać URL i kodują ją przy wysyłaniu, więc 中文 w linku staje się %E4%B8%AD%E6%96%87 na przewodzie. Skopiowany z paska adresu URL może mieć jedną albo drugą postać, zależnie od przeglądarki.

„Każdy escape tutaj wygląda poprawnie, ale wychodzący znak nie jest tym, którego się spodziewałem. Dlaczego?”

Za "zdekodowało się źle" kryją się dwie różne awarie, a strona podaje je inaczej. Pierwsza to uszkodzony escape: znak procentu, po którym nie ma dwóch cyfr szesnastkowych, jak na końcu 100% albo %zz. To problem składniowy: odczyt ścisły go odrzuca i mówi, gdzie się znajduje, a odczyt luźny zachowuje go jako tekst dosłowny i wypisuje. Druga to escape, którego bajt jest w pełni legalny, ale bajty nie składają się w poprawny znak UTF-8, jak %FF samo w sobie albo trzybajtowy znak urwany jako %E4%B8. To nie jest problem składniowy, więc oba odczyty zwracają tekst, zastępują bajty, których nie da się zdekodować, znakiem zastępczym U+FFFD ("�") i wypisują ostrzeżenie, które to mówi. To ostrzeżenie jest najbardziej użyteczną częścią: tekst przychodzący jako znaki zastępcze zwykle oznacza, że nadawca użył innego zestawu znaków albo że ciąg został zakodowany o jeden raz za dużo. Kodowanie procentowe nazywa bajty, a ta strona, podobnie jak standard URL, zawsze czyta te bajty jako UTF-8.

„Czego ta strona nie robi?”

Nie dzieli URL na schemat, host, ścieżkę i zapytanie — do tego służy Parser URL. Nie sprawdza, czy link jest bezpieczny do otwarcia, niczego nie kontaktuje i nie zgaduje zestawu znaków bajtów stojących za escape'em: zawsze czyta je jako UTF-8, czego wymaga standard URL.

„Jakiego zapasowego odczytu wymaga dekodowanie i skąd wiadomo, że jest poprawny?”

Naciśnij Autotest. Porównuje własne kodery strony z encodeURIComponent, encodeURI i URLSearchParams platformy dla każdego drukowalnego znaku ASCII, ponownie uruchamia każdy przykład z tabel powyżej i ponownie sprawdza zapisane łańcuchy warstw, a następnie podaje tutaj liczbę sprawdzeń.

Standardy i zakres

Kodowanie procentowe jest opisane w RFC 3986, sekcja 2.1, z definicją znaków zarezerwowanych i nierezerwowanych w sekcjach 2.2 i 2.3 oraz regułą wielkości liter w sekcji 6.2.2.1. Zbiory, których przeglądarka faktycznie używa przy budowaniu URL, są w standardzie URL WHATWG, który dzieli je według kontekstu: jeden zbiór dla fragmentu, jeden dla zapytania, jeden dla zapytania specjalnego, jeden dla ścieżki, jeden dla danych użytkownika i węższy, używany dla pojedynczego komponentu. Kodowanie formularza, łącznie z plusem, jest w HTML Standard pod application/x-www-form-urlencoded. Dwie funkcje encodeURI i encodeURIComponent należą do samego ECMAScript.

Ta strona wypełnia kolumny ścieżki, zapytania i fragmentu z własnych zbiorów dla kontekstu i wypisuje dokładną listę dla każdego kontekstu obok wyniku. Te zbiory są wyprowadzone ze zbiorów WHATWG i różnią się od nich w dwóch świadomych miejscach: krzyżyk jest kodowany wewnątrz zapytania, bo surowy krzyżyk zaczynałby fragment, a zbiór komponentu jest przypięty dokładnie do znaków, które koduje encodeURIComponent. Tam, gdzie funkcja platformy jest definicją odpowiedzi — encodeURIComponent, encodeURI i serializacja formularza — ta funkcja jest wywoływana wprost, więc te kolumny pokazują zachowanie przeglądarki, a nie jego reimplementację.

Tekst strony został napisany dla tego serwisu. Każda wartość w tabelach powyżej jest wytwarzana przez kod strony w chwili wczytania i została zweryfikowana względem implementacji platformy w narzędziach testowych tego serwisu; Autotest powtarza to porównanie w przeglądarce.