PHP magic testing string, aneb moje řešení problémů s addslashes, stripslashes, htmlspecialchars a magic_quotes_gpc

Aktualizace: V současnosti již nezastávám názor vyjádřený v tomto článku, že direktiva magic_quotes_gpc by měla být v PHP defaultně zapnutá, resp. že by všechny vstupní parametry skriptu měly projít přes funkci addslashes. Ochrana proti SQL injection je totiž záležitostí databázové vrstvy. O změně mého názoru viz novější článek.

Asi každý, kdo někdy programoval v PHP s databázemi, dříve či později narazil na trojici funkcí addslashes, stripslashes, htmlspecialchars a direktivu magic_quotes_gpc. Co přesně tyto funkce a direktiva dělají, tu teď vysvětlovat nechci – dovolím si přepokládat, že je důvěrně znáte. (Pokud ne, asi nemá cenu číst tento text dál.) Ale vsadím se, že stejně jako já na jejich správné použití při programování formulářů ve webových aplikacích pořád zapomínáte. Díky tomu se do aplikací mohou dostat potenciálně docela vážné chyby.

Problém se zapomínáním mě docela štval, a tak jsem ho nakonec vyřešil: Vymyslel jsem testovací řetězec, který ve svých aplikacích zkouším zadávat do každého textového políčka. Pokud se mi podaří formulář odeslat a řetězec se pak někde jinde v aplikaci opět zobrazí v přesně stejné podobě, vše funguje tak jak má. Pokud po odeslání formuláře databáze oznámí chybu nebo se později řetězec zobrazí nesprávně, evidentně je v kódu aplikace něco špatně.

"Magický" řetězec pro testování aplikací tvoří tato posloupnost znaků:

'"\'\" 

Jak vidíte, vlastně to není vůbec nic objevného.

Řetězec dovede detekovat chybné použití i nepoužití libovolné z výše uvedených 3 funkcí. Proč a jak to funguje? Rozeberme si to na dvou případech, při zapnuté a vypnuté direktivě magic_quotes_gpc:

1. magic_quotes_gpc je zapnutá

Pokud ukládáme data ze vstupních parametrů do databáze, není třeba v tomto případě před vložením do databáze s daty dělat nic. Pokud bychom v kódu omylem předpokládali, že direktiva je vypnuta, a data ze vstupu prohnali funkcí addslashes, řetězec se do databáze uloží ve tvaru \'\"\\\'\\\"  a při jeho pozdějším zobrazení v aplikaci to poznáme.

Pokud předaný parametr ještě v témže skriptu zobrazujeme někde na stránce, musíme na něj nejdříve použít funkci stripslashes. Když to neuděláme, vypíše se na výstup zkomolený řetězec \'\"\\\'\\\"  a opět vidíme chybu.

2. magic_quotes_gpc je vypnutá

V tomto případě je nutné vstupní data před vložením do databáze escapovat pomocí addslashes. Pokud to neuděláme, první dva znaky řetězce (jednoduché a dvojité uvozovky) způsobí téměř jistě chybu při dotazu a skript nás upozorní příslušným hlášením.

Pokud předaný parametr ještě v témže skriptu zobrazujeme někde na stránce, není třeba na něj tentokrát volat stripslashes. Pokud to omylem uděláme, vypíše se na výstup řetězec '"'"  a chyba je opět vidět.

V obou případech (při zapnuté i vypnuté direktivě magic_quotes_gpc) řetězec pomáhá kontrolovat, zda při jeho zápisu na výstup používáme funkci htmlspecialchars (samozřejmě pokud v aplikaci hodnotu dotyčné proměnné nechceme interpretovat jako HTML kód). V případě, že na tuto funkci zapomeneme, místo   se na výstupu zobrazí mezera.

"Filozofický" dodatek

Když už jsme u tohoto tématu, tak tu řeknu svůj názor na magic_quotes_gpc. Hlavním důvodem existence této direktivy je ochrana proti SQL injection, čili vložení příkazů pro databázový engine pomocí GET/POST parametrů. Pokud je zapnuta, tak je každý PHP skript proti této ochraně defaultně imunní. Cenou za to je komolení vstupů, které se musí odstraňovat přes stripslashes, což hodně vadí u nedatabázových aplikací.

Zpočátku mě chování skriptů při zapnuté direktivě dost štvalo (a jsou lidé, které to štve i po letech programování v PHP), ale je třeba si uvědomit základní fakta:

  1. Buď budu muset ručně (a) přidávat lomítka při vstupu do databáze nebo (b) lomítka odmazávat při výstupu do HTML. Vynechat obojí nejde.
  2. Zapomenutí escapování u (a) může mít fatální následky (SQL injection), zapomenutí odescapování u (b) jen sem tam zobrazí nějaké to lomítko navíc.
  3. Na výstupu je u textových řetězců skoro vždy potřeba použít htmlspecialchars, na nutnost dalšího odescapování tedy zapomenu pravděpodobně méně než u vstupu do databáze (psychologicky je menší rozdíl mezi "dělat něco" a "dělat víc" než mezi "dělat něco" a "nedělat vůbec nic").
  4. Databázových aplikací je v PHP opravdu velká většina, takže potřeby nedatabázových aplikací jsou vcelku podružné.

Z toho všeho je asi vcelku jasný můj názor na správné defaultní nastavení direktivy magic_quotes_gpc :-)

Code, code, code

Na závěr přidám malý kousek kódu, který se hodí v případě, že nevíte (nebo nemůžete ovlivnit), jak bude na serveru nastavena direktiva magic_quotes_gpc, nebo chcete kód napsat obecněji, tj. aby nezávisel na tomto nastavení (např. pokud programujete knihovnu). Kód je vhodné umístit někam na začátek skriptu. Jeho činnost spočívá v tom, že zjistí stav direktivy magic_quotes_gpc a všechny vstupní parametry případně addslashuje, takže zbytek skriptu se může chovat, jako kdyby byla direktiva zapnuta, a nemusí se o její opravdovou hodnotu vůbec starat.

if (!get_magic_quotes_gpc()) {

  foreach ($_GET as $key => $value) {
    if (is_array($_GET[$key])) {
      foreach ($_GET[$key] as $key2 => $value2) {
        $_GET[$key][$key2] = addslashes($_GET[$key][$key2]);
      }
    } else {
      $_GET[$key] = addslashes($_GET[$key]);
    }
  }

  foreach ($_POST as $key => $value) {
    if (is_array($_POST[$key])) {
      foreach ($_POST[$key] as $key2 => $value2) {
        $_POST[$key][$key2] = addslashes($_POST[$key][$key2]);
      }
    } else {
      $_POST[$key] = addslashes($_POST[$key]);
    }
  }

  foreach ($_COOKIE as $key => $value) {
    if (is_array($_COOKIE[$key])) {
      foreach ($_COOKIE[$key] as $key2 => $value2) {

        $_COOKIE[$key][$key2] = addslashes($_COOKIE[$key][$key2]);
      }
    } else {
      $_COOKIE[$key] = addslashes($_COOKIE[$key]);
    }
  }

}