Na navigaci | Klávesové zkratky

Translate to English… Ins Deutsche übersetzen…

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 třeba Cross Site Scripting (XSS) nebo SQL injection, patří bohužel 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.

Příklad: do řetězce ohraničeného uvozovkami chceme zapsat uvozovky. Jelikož uvozovky mají v kontextu řetězce speciální význam a jejich prosté zapsání by bylo chápáno jako ukončení řetězce, je potřeba je zapsat jinou odpovídající sekvencí. Jakou přesně určují pravidla kontextu.

Předpoklady

Každá escapovací funkce předpokládá, že vstupem je vždy surový řetězec v určitém kódování (znakové sadě).

Odbočka: PHP obsahuje historický relikt nazývaný magic quotes. Pokud jsou magic quotes aktivní, PHP při spuštění automaticky zavolá nad takřka všemi vstupními řetězci v superglobálních polích funkci addslashes, čímž je pozmění a znehodnotí pro další zpracování. Magic quotes vznikly na základě chybného předpokladu, nemají žádný pozitivní přínos, ba právě naopak. 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é modifikace zvrátí a budeme mít opět k dispozici vstup v surovém tvaru.

Mimochodem ze stejného důvodu je také kontraproduktivní třeba ukládat do databáze řetězce již předem escapované pro HTML výstup a podobně.

S jakými kontexty se setkáváme?

Jak bylo řečeno, escapování převádí znaky mající v určitém kontextu speciální význam. 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í znaková sada s 1bajtovým nebo UTF-8 kódováním

HTML

V HTML kontextech mají souhrnně speciální význam znaky < & " ' a odpovídající sekvence jsou &lt; &amp; &quot; &#039;. 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ářů (tj. neumí nahradit dvojici -- za něco jiného).

Reverzní funkce může plnohodnotně fungovat pouze s UTF-8 (vyžaduje 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

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)

V řetězci, kterým se hledaný výraz nahrazuje (tedy například 2. parametr funkce preg_replace), má speciální význam zpětné lomítko a dolar:

addcslashes($replacement, '$\\')

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 kódu lze využít funkci var_export.

Poznámka: protože zmíněné regulární výrazy se obvykle zapisují uvnitř 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. Téměř vždy je ale dostupná jen funkce pro escapování řetězců a tu nelze použít k ničemu jinému, zejména chybí funkce escapující zástupné znaky používané v konstrukcích LIKE (v MySQL jde o znaky % _) nebo identifikátory, jako jsou názvy tabulek či sloupců. Databáze nevyžadují odstraňování escapování na výstupu! (S výjimkou např. typu bytea.)

Znakové sady s neobvyklým vícebajtovým kódováním je nutné v MySQL nastavit funkcí mysql_set_charset resp. mysqli_set_charset.

Doporučuji používat databázový layer (např. dibi, PDO) nebo parametrické dotazy, které escapování obstarají 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á navíc obalí řetězec do uvozovek. Striktně vyžaduje UTF-8.

JavaScript zapsaný uvnitř HTML atributů (např. onclick) je nutné ještě escapovat podle HTML pravidel, neplatí to však pro JavaScript uvnitř značek <script>, kde musí být ošetřen pouze případný výskyt koncové značky </script> uvnitř řetězce. To ovšem funkce json_encode zajistí, jelikož JSON escapuje lomítko /. Neošetří však konec HTML komentáře --> (což v HTML nevadí) nebo XML bloku CDATA ]]>, do kterého se skript obaluje. Pro XML/XHTML je řešením

str_replace(']]>', ']]\x3E', json_encode($s))

Jelikož JSON využívá podmnožinu syntaxe JavaScriptu, je reverzní funkce json_decode plně použitelná jen pro JSON, pro JavaScript 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 a jeho escapování uvnitř HTML atributů a značek (zde se jedná o atributy style a značky <style>).

URL

V kontextu URL se escapuje vše 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án zápis znaků v kódování UTF-8 (zdaleka ne každý web to dodržuje).

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 Latte.

Komentáře

  1. v6ak http://v6ak.profitux.cz/ #1

    „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)

    před 8 lety | reagoval [2] David Grudl
  2. David Grudl http://davidgrudl.com #2

    avatar

    #1 v6aku, 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.

    před 8 lety | reagoval [3] v6ak
  3. v6ak http://v6ak.profitux.cz/ #3

    #2 Davide Grudle, „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ší:

    'preg_match('.var_export('...'.preg_quote($string).'...').', $subject)'

    „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.

    před 8 lety
  4. Aichi http://www.czechdesign.cz/blogs/aichi/ #4

    avatar

    super, zkusis jeste rozvest krizeni obsahu? vkladani HTML do db, atd? budu to pak vsem davat jako povinnou cetbu

    před 8 lety
  5. Bohdan #5

    avatar

    „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?

    před 8 lety | reagoval [6] David Grudl
  6. David Grudl http://davidgrudl.com #6

    avatar

    #5 Bohdane, 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.

    před 8 lety
  7. Almad #7

    Ad databáze, proč nepoužívat parametrizované dotazy, které by měly escapovat samy…?

    před 8 lety
  8. Ludek #8

    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?

    $html = '<span style="color: #000;" class=\'notice\'></span><!-- <div>comment</div> -->';
    echo htmlspecialchars($html, ENT_QUOTES);
    // &lt;span style=&quot;color: #000;&quot; class=&#039;notice&#039;&gt;&lt;/span&gt;&lt;!-- &lt;div&gt;comment&lt;/div&gt; --&gt;

    Coz je dle meho nazoru v poradku.
    Dik

    před 8 lety | reagoval [9] David Grudl
  9. David Grudl http://davidgrudl.com #9

    avatar

    #8 Ludku, ano, to bude fungovat správně. Ale třeba

    echo '<!-- ', htmlspecialchars($s), ' -->';

    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.

    před 7 lety | reagoval [10] Ludek
  10. Ludek #10

    #9 Davide Grudle, Podle me by to melo byt ok.

    echo 'text<!-- ' . htmlspecialchars('comment -->', ENT_QUOTES) . ' -->text';

    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.

    před 7 lety | reagoval [11] David Grudl
  11. David Grudl http://davidgrudl.com #11

    avatar

    #10 Ludku, 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ží.

    před 7 lety

Tento článek byl uzavřen. Už není možné k němu přidávat komentáře.