Escapování - definitivní příručka
Největší programátorský evergreen jsou zmatky a nejasnosti kolem escapování. Neznalost způsobuje, že nejtriviálnější metody narušení webových stránek, jako je Cross Site Scripting (XSS) nebo SQL injection, patří mezi nejrozšířenější.
Escapování je převod znaků majících v daném kontextu speciální význam na jiné odpovídající sekvence.
Jak na escapování?
Každá escapovací funkce předpokládá, že vstupem je vždy surový řetězec v určité znakové sadě (kódování).
Ale pozor: PHP obsahuje historický relikt nazývaný magic quotes. Pokud
jsou magic quotes aktivní, PHP při spuštění automaticky zavolá nad
téměř všemi vstupními řetězci v superglobálních polích funkci
addslashes. Magic quotes vznikly na základě chybného
předpokladu, nemají žádný přínos, ba právě naopak: pozmění vstupní
řetězce a tím je poškodí pro další zpracování. V novějších verzích
PHP budou magic quotes odstraněny. Zatím je nutné je vypínat (např.
požádat hostera o vypnutí) nebo povinně na začátku spouštěného skriptu
zavolat tento
kód, který způsobené změny zvrátí.
S jakými kontexty se setkáváme?
Pro každý kontext se používají jiné escapovací funkce. Tato tabulka je pouze orientační, je nutné si přečíst poznámky níže.
| kontext | escapovací funkce | reverzní funkce |
|---|---|---|
| HTML | htmlspecialchars | html_entity_decode |
| XML | htmlspecialchars | — |
| regulární výraz | preg_quote | — |
| PHP řetězce | var_export | — |
| MySQL databáze | mysql_real_escape_string | — |
| MySQL improved | mysqli_real_escape_string | — |
| SQLite databáze | sqlite_escape_string | — |
| PostgreSQL databáze | pg_escape_string | — |
| PostgreSQL, typ bytea | pg_escape_bytea | pg_unescape_bytea |
| JavaScript, JSON | json_encode | json_decode |
| CSS | addcslashes | — |
| URL | rawurlencode | urldecode |
Vysvětlení k následujícím poznámkám:
- řada kontextů má své podkontexty a v nich se escapování liší. Nebude-li řečeno jinak, je uvedená escapovací funkce použitelná plošně bez dalšího rozlišování podkontextů.
- pod pojmem obvyklá znaková sada se rozumí 1bajtové nebo UTF-8 kódování
HTML
V HTML kontextech mají souhrnně speciální význam znaky < &
" ' a odpovídající sekvence jsou < & "
'. Výjimkou je HTML komentář, kde má speciální význam jen
dvojice --. K escapování se používá:
htmlspecialchars($s, ENT_QUOTES)
Funguje s libovolnou obvyklou znakovou sadou, jiné je třeba konzultovat s manuálem. Ale pozor, tato funkce nezohledňuje podkontext HTML komentářů.
Reverzní funkce může plnohodnotně fungovat pouze se znakovou sadou UTF-8 a to pod PHP 5:
html_entity_decode($s, ENT_QUOTES, 'UTF-8')
XML / XHTML
XML 1.0 se od HTML liší v tom, že zakazuje použití kontrolních znaků
C0 (a to včetně zápisu v podobě entity) s výjimkou tabulátoru,
odřádkování a mezery. XML 1.1 tyto zakázané znaky s výjimkou
NUL v podobě entit naopak povoluje a dále přikazuje kontrolní
znaky C1 s výjimkou NEL taktéž zapisovat jako entity. Dále
v XML má speciální význam sekvence ]]>, proto je třeba
jeden z těchto znaků také escapovat.
Pro XML 1.0 a libovolnou obvyklou znakovou sadu tak lze použít (zdroj © Nette Framework):
htmlspecialchars(preg_replace('#[\x00-\x08\x0B\x0C\x0E-\x1F]+#', '', $s), ENT_QUOTES)
Regulární výraz
V Perlových regulárních
výrazech mají souhrnně speciální význam znaky . \ + * ? [ ^ ] $
( ) { } = ! < > | : a tzv. delimiter, což je znak ohraničující
regulární výraz (např. pro výraz '#[a-z]+#i' je to
#). Escapuje se znakem \.
preg_quote($s, $delimiter)
Kódování musí být buď 1bajtové nebo UTF-8, podle modifikátoru v regulárním výrazu. Viz také Escapování v regulárních výrazech.
PHP řetězce
PHP rozlišuje dva typy řetězců:
- v jednoduchých uvozovkách a NOWDOC, kde speciální význam mohou mít
znaky
\ ' - ve dvojitých uvozovkách a HEREDOC, kde speciální význam mohou mít
znaky
\ " $
Escapuje se znakem \. To obvykle provádí programátor při
psaní kódu, pro generátory PHP lze využít funkci var_export.
Poznámka: protože zmíněné regulární výrazy se většinou zapisují
jako PHP řetězce, je potřeba zkombinovat obě escapování. Např. znak
\ se pro regulární výraz zapíše jako \\ a v PHP
souboru je třeba psát \\\\. Viz také Regulární korektura
Intervalu.cz.
SQL a databáze
Každá databáze má svou vlastní escapovací funkci, viz tabulka výše.
Žádná z těchto funkcí neescapuje zástupné znaky používané
v konstrukcích LIKE (v MySQL jde o znaky % _).
Databáze nevyžadují žádné výstupní odstranění escapování!
(S výjimkou např. typu bytea.)
Neobvyklou vícebajtovou znakovou sadu je nutné v MySQL nastavit funkcí mysql_set_charset resp. mysqli_set_charset.
Doporučuji používat databázový layer (např. dibi, PDO), který escapování obstará automaticky za vás.
JavaScript, JSON
Jakožto programovací jazyk má řadu velmi odlišných podkontextů. K escapování řetězců lze využít vedlejší efekt funkce
json_encode((string) $s)
která však řetězec obalí do uvozovek. Striktně vyžaduje znakovou sadu UTF-8.
JavaScript uvnitř HTML atributů je nutné ještě escapovat podle HTML
pravidel, což však neplatí pro JavaScript uvnitř značek
<script>. Zde se využívá vlastnosti JSON escapovat
lomítko (forward slash), takže ošetří případnou koncovou značku
</script> v řetězci. Neošetří však konec HTML
komentáře --> (což v HTML nevadí) nebo XML bloku CDATA
]]>, do kterého se skript obaluje. Řešením by bylo
str_replace(']]>', ']]\x3E', json_encode($s))
Jelikož JSON využívá podmnožinu syntaxe JavaScriptu, je reverzní funkce json_decode použitelná jen omezeně.
CSS
V CSS kontextech je rozsah platných znaků přesně vymezen, pro escapování identifikátorů lze použít například tuto funkci (zdroj © Nette Framework):
addcslashes($s, "\x00..\x2C./:;<=>?@[\\]^`{|}~")
Pro CSS uvnitř HTML kódu platí totéž, co bylo řečeno o JavaScriptu.
Uvnitř HTML atributu style je nutné ještě escapovat podle HTML
pravidel, uvnitř značek <style> nikoliv.
URL
V kontextu URL se escapují všechny znaky kromě písmen anglické abecedy,
číslic a znaků - _ . nahrazením za % +
hexadecimálně vyjádřený bajt.
rawurlencode($s)
Podle RFC 2718 (z roku 1999) nebo RFC 3986 (z roku 2005) je preferována znaková sada UTF-8.
Reverzní funkcí je v tomto případě urldecode, která rozeznává i znak
+ s významem mezery.
Všimněte si, že nebyl zmíněn žádný kontext, kde by se využily funkce
addslashes nebo stripslashes – zapomeňte
na ně!
Pokud se vám zdá celá problematika příliš složitá, nezoufejte. Brzy přijdete na to, že jde vlastně o jednoduché tranformace a celý trik spočívá v uvědomění, v jakém kontextu se nacházím a jakou musím pro něj zvolit funkci. Nebo ještě lépe, zkuste použít inteligentní šablonovací systém, který dokáže kontexty rozeznat sám a použít správné escapování: Nette\Template.

#1 v6ak http://v6ak.profitux.cz/ nový
„Doporučuji používat databázový layer (např. dibi), který escapování obstará automaticky za vás.“
To zní jako magie. Nělo by to vyjádřit lépe?
Chybí mi tu výraznější uporoznění na obalování kontextů jako u Javascriptu.
Hodilo by se také větší upozornění na problém znakových sad.
Dál by tu mohl být seznam layerů, kterým se to sype sémantičtěji a dovedou si zařídit escapování (SQL: dibi, PDO; HTML: Nette:HTML, DOMDocument)
#2 David Grudl http://davidgrudl.com nový
#1 v6ak: chybějící informace klidně dopiš do komentářů. Bylo by fajn to podat jazykem srozumitelným laikům, pro které je článek určen.
#3 v6ak http://v6ak.profitux.cz/ nový
#2 David Grudl: „Bylo by fajn to podat jazykem srozumitelným laikům, pro které je článek určen.“
Určitě, ale nevím, jestli to zvládnu.
„Doporučuji používat databázový layer (např. dibi), který escapování obstará automaticky za vás.“ → „Doporučuji používat databázový layer (např. dibi), se kterým bude escapování hračka.“ – to zní méně magicky, ne?
„Chybí mi tu výraznější uporoznění na obalování kontextů jako u Javascriptu.“
Někdy můžete narazit na kontext obalující jiný kontext. Například pokud chcete vygenerovat regulární výraz, do kterého chcete dosadit svůj řetězec, funguje to ještě tak, jak nejspíš čekáte: preg_match(‚…‘.preg_quote($string).‚…‘, $subject). Pokud ale chcete vygenerovat PHP kód s tímto regulárním výrazem, bude to trochu složitější:
„Hodilo by se také větší upozornění na problém znakových sad.“
Pokud pracujete s obvyklou znakovou sadou UTF-8, často nemusíte nic řešit. Pokud ale pracujete s neobvyklou znakovou sadou, je mnohdy nutné se starat o to, aby escapovací funkce dostala správné kódování, jinak bude escapování neúčinné. Zkuste použít UTF-7 a escapovat pro UTF-8 a uvidíte, že mnoho toho projde.
Je samozřejmě vhodné escapování uvádět vždy.
A k tomu seznamu layerů mě už víc momentálně napadá ještě funkce http_build_query pro URL.
#4 Aichi http://www.czechdesign.cz/blogs/aichi/ nový
super, zkusis jeste rozvest krizeni obsahu? vkladani HTML do db, atd? budu to pak vsem davat jako povinnou cetbu
#5 Bohdan bohdan.maslowski@gmail.com nový
„Podle RFC 3986 (z roku 2005) musí být znaková sada UTF-8.“
A nevíte někdo, kde to tam je? Prolistoval jsem to a nenašel.
Nedávno jsem řešil problém s escapováním url pro ARES. Jejich xml služba pro vyhledávání v db ekonomických subjektů má rozhraní
http://wwwinfo.mfcr.cz/cgi-bin/ares/darv_std.cgi?obchodni_firma=<obchodni_firma>z dokumentace (kde o kódování není ani slovo):
<obchodni_firma>– obchodní firma. Doporučujeme nezadávat znaky: * ? & %V praxi je možné použít windows-1250 i iso-8859–2 (opravdu funguje oboje), ale UTF-8 ne.
Zajímavé je, že v prohlížečích to nemá problém. Firefox předpokládám zakóduje url podle kódování stránky (při odeslání formu metodou get), což by ale odporovalo tomu, že to musí být v UTF-8 (!). A když napíšu do url pole něco s diakritikou, převede se to na ISO 8859–2.
viz. hledání firmy „ŠELEST“
iso-8859–2: http://wwwinfo.mfcr.cz/…darv_std.cgi?…
windows-1250: http://wwwinfo.mfcr.cz/…darv_std.cgi?…
UTF-8: http://wwwinfo.mfcr.cz/…darv_std.cgi?…
Je to chyba, nebo jen špatná dokumentace?
#6 David Grudl http://davidgrudl.com nový
#5 Bohdan: pardon, konkrétnější je asi RFC 2718 z roku 1999. V článku jsem to upravil a doplnil.
Rok jsem uváděl, aby bylo patrné, že jde o relativně nové dokumenty (ARES je o pár let starší), takže mohou pouze doporučovat, jak psát nové webové aplikace, ale ty staré už nezmění. Jinými slovy, pokud tvoříte aplikaci, držte se pravidla, že znaková sada musí být UTF-8, pokud komunikujete s jinou aplikací, je třeba se jí přizpůsobit.
#7 Almad nový
Ad databáze, proč nepoužívat parametrizované dotazy, které by měly escapovat samy…?
#8 Ludek nový
Ahoj,
skvely clanek, diky. Kazdopadne jsem nejak nepochopil Ale pozor, tato funkce nezohledňuje podkontext HTML komentářů. u HTML.
Mohl by pls nekdo vysvetlit co je tim mysleno?
Coz je dle meho nazoru v poradku.
Dik
#9 David Grudl http://davidgrudl.com nový
#8 Ludek: ano, to bude fungovat správně. Ale třeba
už ne. Uvnitř HTML komentáře platí odlišná pravidla pro escapování, kritickým znakem jsou zde pomlčky, které se nesmí objevit dvě vedle sebe.
#10 Ludek nový
#9 David Grudl: Podle me by to melo byt ok.
Protoze na ukonceni komentare je potreba prava ostra zavorka. Dve pomlcky sami o sobe nic neznamenaji.
Sice jsem nekde narazil, ze mezi – a > muze byt mezera, ale jak jsem pochopil asi zalezi na DOCTYPE.
Kazdopadne vyse uvedeny priklad jsem vyzkousel ve FF3 a IE7 a vse bylo v poradku (zadny DOCTYPE jsem neuvedl).
Dokonce jsem narazil na to, ze by se komentar mel psat:
<!-- poznámka //-->
Ale asi mas pravdu ze pro 100% bezpecnost by se mely dve pomlcky prevest zrejme na entity nebo odstranit nebo mezi ne vlozit mezera. Tezko rict co je nejvhodnejsi, asi by zalezelo jaka data v tom komentari chceme zobrazit.
Kazdopadne dik za odpoved.
#11 David Grudl http://davidgrudl.com nový
#10 Ludek: problém je v tom, že dvě pomlčky sami o sobě uvnitř komentáře mají význam a záleží na konkrétním browseru a DOCTYPE, jak s nimi naloží.