Přejít k navigační liště

Zdroják » Různé » Automatické zabezpečení

Automatické zabezpečení

Články Různé

Nespoléhejte se na to, že do kódu nezapomenete na všechna místa připsat ošetření dat. Snažte se aplikaci navrhnout tak, aby se na nic zapomenout nedalo. Za cenu o něco složitějšího jádra bude veškerý kód, který ho používá, obvykle taky mnohem jednodušší.

Nálepky:

Článek vyšel na blogu Jakuba Vrány, na Zdrojáku ho přetiskujeme s autorovým souhlasem.

Úroveň zabezpečení aplikace bych rozdělil do tří úrovní:

  1. Aplikace zabezpečená není, neošetřuje uživatelské vstupy ani své výstupy.
  2. Aplikace se o zabezpečení snaží, ale takovým způsobem, že na ně lze zapomenout.
  3. Aplikace se o zabezpečení stará sama, prakticky se nedá udělat chyba.

Jak se tyto úrovně projevují v jednotlivých oblastech?

XSS

Druhou úroveň představuje ruční ošetřování pomocí htmlspecialchars. Třetí úroveň zdánlivě reprezentuje automatické ošetřování v šablonách, např. v Nette Latte. Proč píšu zdánlivě? Problém je v tom, že ošetření se dá obvykle snadno zakázat, např. v Latte pomocí {!$var}. Viděl jsem šablony plné vykřičníků i na místech, kde být neměly. Autor to vysvětlil tak, že psaní {$var} někde způsobovalo problémy, které po přidání vykřičníku zmizely, tak je začal psát všude.

Je to samozřejmě hloupá chyba, ale přesto – jak jí zamezit? Řešením je informaci o ošetření dat uložit přímo do proměnné a v šablonách {!$var} úplně zakázat. V Nette jde data označit jako ošetřená takto:

<?php
$safeHtml = $texy->process($content_texy);
$content = Html::el()->setHtml($safeHtml);
// v šabloně pak můžeme použít {$content}
?>

Ideální by bylo, když by už samotná metoda process() vracela instanci  Html.

Samozřejmě lze namítnout, že chybu lze stále udělat nevhodným použitím Html. Nicméně už alespoň šablony jsou neprůstřelné a jejich autor (který může být míň zkušený) nemusí přemýšlet, která data ošetřit má a která ne.

SQL Injection

Druhou úroveň představuje ruční ošetřování pomocí mysql_real_escape_string nebo obdobné funkce. Třetí úroveň zdánlivě reprezentuje vázání proměnných, např. v PDO. Proč píšu zdánlivě? Problém je v tom, že napsat $pdo->prepare("... WHERE id = $_GET[id]") je funkční a ještě jednodušší než $pdo->prepare("... WHERE id = ?", $_GET["id"]). V některých případech to je dokonce jediné možné řešení, alternativu k $pdo->prepare("... ORDER BY $_GET[order]")  vázání proměnných nenabízí.

Jedno „dokonalé“ řešení jsem před časem nabídl, i když je poněkud nepraktické. NotORM šlo praktičtější cestou, ale výsledek z pohledu SQL Injection je jen o málo lepší než u PDO. Chybu pořád lze udělat, i když bezpečná verze je ve většině případů alespoň jednodušší: where("id", $_GET["id"]) je jednodušší než  where("id = $_GET[id]").

Správné řešení by mohlo vypadat nějak takhle: where("id", "=", $_GET["id"])  – první parametr by byl ošetřen jako identifikátor, druhý by byl z whitelistu a třetí jako hodnota. I třídění podle uživatelem zvoleného sloupce by bylo jednoduché: order($_GET["order"]). Alternativou je identifikátor předat přímo v názvu funkce, např. jako whereIdEquals($_GET["id"]), tam je ale použití obecného sloupce krkolomné ( {"orderBy$_GET[order]"}()).

Problém nastává u složitějších výrazů, např. s operátorem OR, což je jeden z důvodů, proč jsem toto řešení v NotORM nepoužil.

Neautorizovaný přístup k datům

Další běžná chyba, kde je řešení druhé úrovně jednoduché: stačí místo WHERE id = ? použít WHERE id = ? AND user_id = ?  – při získávání dat, zobrazování jejich detailu, jejich aktualizaci a mazání. Problém je, že na tohle se dá zapomenout velmi snadno.

Řešení třetí úrovně se v tomto případě hledá hůř. Místo jednoho ID platného pro všechny můžeme použít dvojici user_id, number. Když má uživatel třeba články a články mají komentáře, tak u komentáře bude primární klíč user_id, article_number, number. Některé dotazy to dokonce může zjednodušit – např. pro vypsání všech komentářů daného uživatele se obejdeme bez použití tabulky článků.

Pokud žádné ID uživatele uložené nemáme, dá se místo snadno uhodnutelného číselného ID použít dlouhé náhodné GUID.

Označování dat

Do PHP byla kdysi navržena podpora pro značkování řetězců, která nakonec vznikla jako extenze Taint. Za její zásadní nevýhodu považuji především to, že jako nebezpečná označuje jen data, která pochází bezprostředně od uživatele. Takže když vezmu $_POST parametr, ošetřím ho pro uložení do databáze, uložím a načtu, tak je najednou bezpečný i třeba pro výstup do HTML nebo pro nové uložení do databáze. Já bych byl radikálnější a jako nebezpečná data označil všechno, to by ale samozřejmě zcela zlikvidovalo zpětnou kompatibilitu.

Ošetřování dat

Za ideální považuji aplikaci, kde vývojář vůbec nemusí vědět, na jaké speciální znaky si musí dát pozor a jaké má k jejich ošetření použít funkce. Třeba dotaz prepare("WHERE name = 'admin'") je z tohoto pohledu špatně, protože musím vědět, že pro ohraničení řetězců se používají apostrofy. Dotaz prepare("WHERE name = ?", "admin") je lepší, pořád si ale musím dávat pozor na správné ošetření sloupce. U where("name", "=", "admin")  nemusím myslet na nic.

Závěr

Nespokojte se s druhou úrovní zabezpečení a nespoléhejte se na to, že na ošetření určitě nezapomenete. Snažte se aplikaci navrhnout tak, aby se na nic zapomenout nedalo. Za cenu o něco složitějšího jádra bude veškerý kód, který ho používá, obvykle taky mnohem jednodušší.

Ono se to ostatně netýká jen bezpečnosti, ale třeba i výkonnosti. V NotORM je docela těžké napsat kód tak, aby se dotazy nepokládaly optimálně nebo téměř optimálně (při správně vytvořených indexech v databázi). Naproti tomu třeba framework Laravel jde opačnou cestou – základ je pomalý a když chcete zrychlit, musíte napsat kód navíc.

Komentáře

Subscribe
Upozornit na
guest
10 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
mhlavac

Mezi námi je furt spousta vývojářů, kteří neví co je zabezpečování dat. Nejlepší radou pro ně je použít nějaký již hotový framework. Problém těchto vývojářů je ten, že se ho učit nechtějí a tak radši věnují svůj čas na věčnou implementaci stejných částí furt dokola a dokonce znova a znova špatně.

Co se týče zabezpečení SQL dotazů tak celkem zajimavým nástrojem je QueryBuilder, který používá Doctrine 2. Bohužel jako každá abstraktní vrstva i tady to má menší vliv na výkon, ale ve výsledku za to použití to stojí.

Pro dostatečné základy ohledně bezpečnosti stačí prolítnout Symfony 2 dokumentaci, která obsahuje údaje o tom jak zabezpečit stránku v jednoduché formě. Problém ale přetrvává, jedná se o další čtení o které velká část programátorů nemá zájem, protože jejich způsob jim dostačuje.

okbob

nebylo by lepší programátory dokopat aby si něco málo o bezpečnosti přečetli a místo psaní „dokonalých“ neprůstřelných frameworků přidat kontroly s warningy na případné bezpečnostní chyby – třeba SQL injection by možná bylo kontrolovatelné poměrně snadno.

Aleš Roubíček

Prosím, začněte kopat.

Futrál

Komu není rady, tomu není pomoci. A rad už je na webu hodně.

Futrál

Taky že je – viz třeba Findbugs, který najde SQL dotazy lepené z nekonstantních řetězců.

Monty

>> Dotaz prepare(„WHERE name = ?“, „admin“) je lepší, pořád si ale musím dávat pozor na správné ošetření sloupce.
To nechápu. Jaké ošetření sloupce mám v tomto dotazu provést?

bene

Osobně se mi řešení pro neautorizovaný přístup k datům přes WHERE id = ? AND user_id = ? moc nelíbí. Získání pouze přes id vetšinou už je hotové např. kvůli „superadminovi“ nebo nám to nabízí databázová vrstva. Psát další SQL dotazy ať už formou čístého SQL nebo „fluent“ nástroje by se mi nechtělo. Navíc taková věc lze někde zapomenout.

Raději bych vytvořil nějakou UserFacade($u­serId), která by nabízela jen ty metody pro vracení dat, které daný uživatel skutečně získat může a ošetření na user_id bych udělal v ní. Pak budu v aplikaci používat skutečně bezpečnou vrstvu kde možnost udělat chybu je pouze nepoužít tuto vrstvu a to je pak už diletanství.

Možná to vypadá jako totéž (udělám si DB vrstvu, která je jen pro uživatele), ale UserFacade by využívala hotovou DB vrstvu a jen by kontrolovala user_id (samozřejmě v některých případech by musela být DB vrstva dopněna o nějaké dotazy v případě, že je user_id k dispozici např. přes join).

public fucntion getArticle($id) {
    $article = $this->repository->getArticle($id);
    if ($article->user_id != $this->userId) {
        throw new UnauthorizedException;
    }
    return $article;
}
5o

ACL dynamic assertion?

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.