Lekce 6 - Obrana proti útoku Mass assignment v PHP
V předchozí lekci, Útok CSRF (Cross Site Request Forgery) a jak se bránit, jsme se seznámili s útokem Cross Site Request Forgery a také jsme si uvedli způsoby, jak před tímto útokem svou aplikaci chránit.
Mass assignment je útok, který se z angličtiny překládá jako hromadné přiřazení. Představme si následující situaci:
Máme na webové stránce registrované uživatele a chceme jim poskytnout možnost změny údajů. V databázi máme tabulku s uživateli:
S tímto jedním testovacím uživatelem:
Vytvoříme jednoduchý formulář (proměnné se vytáhnou z databáze).
<h2>Editace údajů</h2> <form method="post"> <table> <tr><th>Jméno</th></tr> <tr><td><input type="text" name="firstname" value="<?= htmlspecialchars($firstname) ?>" /></td></tr> <tr><th>Příjmení</th></tr> <tr><td><input type="text" name="lastname" value="<?= htmlspecialchars($lastname) ?>" /></td></tr> <tr><th>Přezdívka</th></tr> <tr><td><input type="text" name="nick" value="<?= htmlspecialchars($nick) ?>" /></td></tr> <tr><td><input type="submit" value="Změnit údaje" /></td></tr> </table> </form>
Vypadá asi takto:
Předpokládejme, že pracujeme s nějakým databázovým wrapperem (ono to bez něj v dnešní době již ani moc nejde), který obsahuje metodu pro update:
class Database extends PDO { // hromada kódu /** * @param string * @param array * @param string|NULL * @return PDOStatement */ public function update($table, $data, $where = NULL) { $sql = "UPDATE {$table} SET "; $counter = 0; $dataCount = count($data); foreach ($data as $column => $value) { $value = $this->quote($value); $sql .= "{$column} = {$value}"; if ($counter < $dataCount - 1) { $sql .= ", "; } $counter++; } if ($where !== NULL) { $sql .= " WHERE {$where}"; } return $this->query($sql); } }
Pokud například zavoláme
$db = new Database("přihlašovací údaje"); $db->update("user", array( "firstname" => "jméno", "lastname" => "příjmení", "nick" => "přezdívka" ), "id = 1");
, vygeneruje nám to tento SQL dotaz:
UPDATE user SET firstname = 'jméno', lastname = 'příjmení', nick = 'přezdívka' WHERE id = 1
Nyní, protože jsme velmi líní, zpracujeme formulář tímto způsobem:
<?php $db = new Database("přihlašovací údaje"); $userId = (int) $_SESSION["userId"]; $errors = array(); if ($_POST) { if (empty($_POST["firstname"])) { $errors[] = "Nebylo vyplněno jméno."; } if (empty($_POST["lastname"])) { $errors[] = "Nebylo vyplněno příjmení."; } if (empty($_POST["nick"])) { $errors[] = "Nebyla vyplněna přezdívka."; } if (empty($errors)) { $db->update("user", $_POST, "id = {$userId}"); // Uložení zprávy o editaci, přesměrování } }
Upravíme údaje v textových polích a formulář odešleme.
Metoda Database::update() sestaví SQL dotaz následovně:
UPDATE user SET firstname = 'Jméno', lastname = 'Příjmení', nick = 'nick' WHERE id = 1
Takové volání metody je velmi snadné, stačí vyplnit 2-3 krátké parametry. Jenže zápis druhého parametru je velmi nebezpečný. Proměnná $_POST pochází od uživatele a můžeme jí data podstrčit a to hned několika způsoby.
Vraťme se zpět na editační formulář. Ukážeme si jeden primitivní způsob, jak data podstrčit, a to přes úpravu HTML formuláře. Podstrčený input vytvoříme jako textové pole, aby byl pro ukázku vidět:
Pokud následně formulář odešleme, metoda nám najednou vygeneruje takový SQL dotaz:
UPDATE user SET firstname = 'Martin', lastname = 'Konečný', nick = 'pavelco1998', admin = '1' WHERE id = 1
Když se následně podíváme do tabulky 'user', uvidíme upravený údaj ve sloupci 'admin'.
Tento příklad útoku by šel provést i tak, že jednoduše přepíšeme atribut některého z textových polí (např. z name="firstname" na name="admin") a změnili hodnotu na 1.
Jak útoku předcházet?
Možností je hned několik.
1. Vypsání hodnot ručně
// zpracování formuláře $data = array( "firstname" => $_POST["firstname"], "lastname" => $_POST["lastname"], "nick" => $_POST["nick"] ); $db->update("user", $data, "id = {$userId}");
2. Vytvořit si seznam povolených hodnot
$allowed = array("firstname", "lastname", "nick");
Díky tomuto poli lze i zkrátit naši kontrolu vyplněných hodnot:
$errors = array(); if ($_POST) { foreach ($allowed as $postKey) { if (empty($_POST[$postKey])) { $errors[] = "Nebyla vyplněna hodnota pole '{$postKey}'"; } } }
Pro samotný update využijeme dvou funkcí:
- array_flip() - přehodí v poli klíče a hodnoty
- array_intersect_key() - vybere pouze ty hodnoty, jejichž klíče jsou stejné v obou polích
$data = array_intersect_key($_POST, array_flip($allowed)); $db->update("user", $data, "id = {$userId}");
3. Využít formulářovou knihovnu, která automaticky vrací jen správné hodnoty
Pro ukázku využijeme Davidovu třídu Form:
$form = new Form("userUpdate"); $form->addTextBox("firstname", "Jméno", TRUE); $form->addTextBox("lastname", "Příjmení", TRUE); $form->addTextBox("nick", "Přezdívka", TRUE); $form->addButton("update", "Změnit údaje"); // hodnoty se opět vyberou z databáze $form->setData(array( "firstname" => $firstname, "lastname" => $lastname, "nick" => $nick )); if ($form->isPostBack()) { try { $db->update("user", $form->getData(), "id = {$userId}"); } catch (UserException $e) { echo "<span style='color: red;'>" . nl2br($e->getMessage()) . "</span>"; } }
Nyní se nemůže stát, že by se v tabulce upravily hodnoty jiných sloupců.
Závěrem
Nikdy, ale opravdu nikdy nevěřte uživateli, který navštíví vaše stránky. Vždy dbejte na ochranu vaší aplikace, protože nikdy nevíte, jaký problém z toho může vzniknout. HTML si každý může ve svém prohlížeči upravit, proto by pro tento příklad nefungovala ani kontrola v JavaScriptu - jednoduše by si ho mohl uživatel ze stránky odstranit nebo vypnout.
V další lekci, Útok Clickjacking a jak se před ním bránit, se seznámíme s útokem Clickjacking a uvedeme si způsoby, jak se před tímto typem útoku bránit.