Translate: 
EnglishFrenchGermanItalianPolishPortugueseRussianSpanish

UTF-8 i zła walidacja tekstu

Jak dobrze wiadomo, słabą stroną języka PHP jest między innymi wsparcie dla bardzo popularnego w dzisiejszych czasach kodowania znaków w UTF-8. O ile w wersji 5.3 pojawiła się biblioteka ext/intl, to jednak większość istniejących mechanizmów walidacji tekstu opiera się wyłącznie na regułkach zapisanych w postaci wyrażeń regularnych. Mechanizm ten jest bardzo wygodny, lecz w przypadku znaków spoza alfabetu łacińskiego nie zawsze działa tak, jak programista to przewidział.

W skrajnych przypadkach użycie wyrażeń regularnych prowadzi to do bardzo ciekawych skutków ubocznych, które mogą negatywnie wpłynąć na bezpieczeństwo serwisu internetowego.

Jak działa walidacja w wyrażeniach regularnych?

Załóżmy, że chcemy zwalidować imię użytkownika wpisane do formularza rejestracyjnego, pozwalamy przy tym wyłącznie na polskie, oraz łacińskie litery. Regułka wygląda więc następująco:

if(!preg_match('~^[A-ZĄĆĘŁŃÓŚŹŻ]{1}[a-ząćęłńóśźż]{2,}$~',$name)) {
	throw new Exception('Wrong user name');
}

Tutaj pierwszy ból, język PHP nie rozumie narodowych zakresów znaków, wskazanie [a-z] w wyrażeniu regularnym nie zawiera w sobie polskich liter, dlatego też musimy wymienić je dodatkowo.

Drugim, znacznie ważniejszym problemem jest to, że powyższa formułka w kodowaniu UTF-8 po prostu nie zadziała jak powinna!

Gdzie leży problem?

Przekonajmy się na przykładzie:

$name = "Łukasz";
if(!preg_match('~^[A-ZĄĆĘŁŃÓŚŹŻ]{1}[a-ząćęłńóśźż]{2,}$~',$name)) {
	throw new Exception('Wrong user name');
}

Walidator w takim przypadku rzuci wyjątkiem, imię wg. tej regułki jest nieprawidłowe. Czemu? Przecież wskazaliśmy, że imię składa się z jednej wielkiej litery oraz przynajmniej dwóch małych, a wyraz „Łukasz” spełnia te warunki.

Okazuje się, że jednak nie do końca. Kodowanie UTF-8 zapisuje znaki narodowe w postaci przynajmniej dwóch bajtów (zależnie od języka, z którego pochodzą).

W przypadku języka polskiego obrazuje to poniższa tabela UTF-8:

Litera Kodowanie
Ą 0xC4 0×84
Ć 0xC4 0×86
Ę 0xC4 0×98
Ł 0xC5 0×81
Ń 0xC5 0×83
Ó 0xC3 0×93
Ś 0xC5 0x9A
Ź 0xC5 0xB9
Ż 0xC5 0xBB
Litera Kodowanie
ą 0xC4 0×85
ć 0xC4 0×87
ę 0xC4 0×99
ł 0xC5 0×82
ń 0xC5 0×84
ó 0xC3 0xB3
ś 0xC5 0x9B
ź 0xC5 0xBA
ż 0xC5 0xBC

Wyrażenie regularne w walidatorze mówi więc, że spodziewamy się jednego bajtu (a nie znaku) zawierającego wielką literę.

W takim przypadku polskie litery, które znajdują się w regułce, rozbijane są na pojedyncze bajty. Zamiast znaku „Ł” funkcja preg_match() szuka w tekście dwóch bajtów: 0xC5 oraz 0×81 składających się na ten znak. Podobnie jest z pozostałymi literami polskiego alfabetu.

W powyższym przykładzie preg_match() znajduje bajt 0xC5 (pierwszy bajt litery Ł), po czym przystępuje do odszukania małych liter w tekście. Tu jednak jako drugi pojawia się bajt 0×81 (czyli drugi bajt litery Ł), nie jest on jednak wymieniony w zakresie opisującym małe litery imienia.

Możliwe reperkusje

W przypadku precyzyjnie napisanych regułek programista dosyć szybko zorientuje się, że coś działa niezgodnie z założeniami projektowymi.

Niestety, gdybyśmy w naszym przykładzie pominęli wymuszanie wielkiej litery jako pierwszej w imieniu, lub imię użytkownika zaczynało się od znaku łacińskiego, nie dowiedzielibyśmy się, że wyrażenie jest niewłaściwe.

Pozwoliłoby to użytkownikowi na wpisywanie tekstów niezgodnych z kodowaniem UTF-8, co w efekcie mogłoby zakończyć się błędem krytycznym w dalszym procesie obróbki danych (np. podczas przesyłania pliku XML do innego systemu w sieci).

Jak się przed tym chronić?

Przede wszystkim należy stosować przełącznik „u” (czyli unicode), który traktuje litery narodowe jako nierozłączną całość (nie rozbija ich na pojedyncze bajty). Oto jak wygląda prawidłowa regułka po zastosowaniu tego przełącznika:

if(!preg_match('~^[A-ZĄĆĘŁŃÓŚŹŻ]{1}[a-ząćęłńóśźż]{2,}$~u',$name)) {
	throw new Exception('Wrong user name');
}

Jednak także i tutaj należy się mieć na baczności: przełącznik „u” działa tylko wtedy, gdy biblioteka wyrażeń regularnych PCRE została skompilowana z parametrem –enable-utf8. Jeśli nie, należy ją zrekompilować.

Tagi: , ,

Dodaj odpowiedź