Mass Assignment v PHP

Počátkem března se Rails komunitou prohnal hurikán jménem @homakov. Škod naštěstí nenapáchal mnoho, jenom se lehce otřel o GitHub a odnesl střechu a plot. Mohl si vzít cokoliv, ale asi by to neunesl (a to asi ani psychicky).

O co přesně šlo? V seznamu návrhových vzorů, které Ruby on Rails implementují najdeme i Active Record, s nímž nám Rails ulehčují práci pomocí přístupu, který se volá Mass Assignment – hromadné přiřazení. Díky standardnímu nastavení Rails bylo možné nastavit jakýkoliv atribut, ne jenom ty povolené – whitelistované. A toho právě Egor Homakov využil. Nechci zde opakovat detaily, ty jsou dobře rozebrány v článku o hacku GitHubu na Zdrojáku, ale přijde mi, že článek trochu nešťastně naznačuje, že nic podobného by se v PHP stát nemohlo. Jasně, že mohlo, úplně v pohodě, zkusím naznačit jak.

Modří už vědí, že tento článek není o register_globals, ačkoliv to je to první, co napadne každého, kdo se učil PHP z Koskovy knihy a nezamrzl v čase. Chtěl bych se zaměřit na následující řádek, určitě jste ho někde už viděli, doufám, že ne ve vlastním kódu.

$data = array_merge($data, $_POST);

Ano, tohle se celkem běžně používá pro skládání klíčů a dat třeba pro SQL příkaz UPDATE. Vy to ale nikdy, ale opravdu nikdy nedělejte. Nepříjemné je, že i když ošetříte veškerá data proti SQL Injection, tak stejně lze útok přes Mass Assignment provést.

Proof of concept pseudokód

<?php
$data = array(
    'admin' => 0,
    'name' => 'default name',
    'date' => '2012-03-06 01:23:45',
);

$_POST = array(
    'admin' => 1,
    'name' => 'foobar',
);

$data = array_merge($data, $_POST);
// $data = $data + $_POST nefunguje, operátor + nepřepisuje klíče

$data_sql = array();
foreach ($data as $key => $value) {
    // sqlescape() je obecná escapovací funkce,
    // ale klidně si sem dejte prepared statements
    $data_sql[] = sqlescape($key) . ' = "' . sqlescape($value) . '"';
}

$values = implode(', ', $data_sql);
echo "UPDATE users SET {$values} WHERE id_user = 123";
?>

Jo, přesně tak. Pokud někdo v post datech pošle admin=1 při editaci jména svého uživatele, tak se stane administrátorem a vy si můžete jít dát panáka. A výpověď.

Ponaučení? Vždy explicitně vyjmenujte, co chcete dělat, upravovat, vkládat, načítat.

Třeba takto:

<?php
$data = array(
    'admin' => 0,
    'name' => $_POST['name'],
    'date' => '2012-03-06 01:23:45',
);
?>

Cílem článku nebylo ukázat, jak to máte psát, ale jak je možné napodobit Mass Assignment v PHP. Jen tu nefunguje žádný whitelisting, takže se musíte spolehnout jen na sebe. Zkrátka to musíte napsat správně a bezpečně.

12 thoughts on “Mass Assignment v PHP
  1. Naznačovat, že by se to v PHP stát nemohlo jsem nechtěl.. Sám jsem již párkrát vidědl konstrukci dibi::update('users', $_POST)->where(...)->execute();

    A ano, tohle je stejná díra, jako ta v rails. Nejedná se o problém frameworku – jedná se o problém jeho užití. Rozdíl je pouze v tom, že o tomto mluvíme dlouho, a nikde ho (snad?) nepropagujeme. Mimo jiné i díky článkům, jako je tento.

    Díky!

  2. Aj, tak to jsem špatně pochopil tuhle větu:

    Jedná se přesně o to, co se řeší v PHP už velice dlouho – kontrola dat, které dostává skript od uživatelů ve formuláři.

    Konkrétně to slovo řeší, to mě mrzí. Tenhle můj post ale není útokem na článek na Zdrojáku.

    Nicméně v Rails je možnost, jak se tomuto bránit, jen defaultně nebyla zaplá, takže to tak trochu chyba frameworku je. Uživatel by na zapínání nečeho, co zabrání problémům neměl myslet, protože na to může zapomenout (a zapomene, evidentně). Chyba použití by pak byla, kdyby někdo whitelistoval všechny atributy.

    Krásnej příklad s dibi, mimochodem, systémově tomu elegantně zabránit bude trochu oříšek, žejo, Davide. :)

  3. Michal Špaček: Ono to asi nebylo myšleno tak, že je to v php nějak systémově řešené, ale tak, že se to neustále novým phpkářům furt říká a všude se to dokola omílá. Tyhle věci jsem díky tomu řešil dávno předtím, než jsem začal “pořádně” programovat…

    U railsů je právě problém, že noví lidé asi neví nic o technologii co je pod tím, a spoléhají na to, že framework za ně všechno magicky udělá.

  4. Díky za komentář, přivedl mě totiž konečně na nosnou myšlenku článku, která by se dala formulovat asi nějak takto:

    PHPkáři, nesmějte se Railsařům, váš kód je taky plnej bezpečnostních chyb a to dokonce hodně podobnejch.

    Čest světlým výjimkám. V zásadě totiž nesouhlasím s tím, že se to neustále novým phpkářům furt říká a všude se to dokola omílá.

  5. Michale, to „systémově tomu elegantně zabránit bude trochu oříšek, žejo, Davide. :)“ je na mě?

    Původně jsem se do diskuse o Mass Assignment nechtěl pouštět, protože jsem vod konkurence, ale když už jsi mě pozval…

    Nejprve: bavit se o PHP vs Rails je jako srovnávat italštinu s Neználkem na měsíci. Srovnávejme tedy jazyk s jazykem a framework s frameworkem.

    PHP, Ruby a jakýkoliv jiný jazyk, který zná datový typ pole, pochopitelně Mass Assignment umožní. A je to v pořádku. Z diskuse bych také vynechal Dibi – svůj bezpečnostní přínos má v tom, že se snaží maximálně zjednodušit sestavování SQL dotazů, které nejsou náchylné na SQL injection, nicméně jde o low-level knihovnu, která o nějakém HTTP requestu netuší a tušit nemůže.

    HTTP request, formuláře a databázi sjednocuje je až framework. A ten má za bezpečnost kopat. Bohužel, autoři Rails se rozhodli si léta pěstovat ve frameworku obří bezpečností díru, jen proto, aby byl framework snazší pro začátečníky. Děsivý přístup! Díky tomu jsou dnes aplikace postavené na Rails děravé jako cedník.

    Ačkoliv se kvůli své lenosti také snažím dělat v Nette vše co nejjednodušší, nikdy to nesmí jít na úkor bezpečnosti. Mass Assignment se proto Nette vůbec netýká. Z Nettích formulářů vždy dostanete jen ta data, která jste explicitně specifikovali. Totéž platí pro parametry předávané přes $_GET. A nelze to ani žádnou direktivou změnit, to je by design.

    Tedy k té narážce: ano, systémově tomu elegantně zabránit jde. Na úrovni frameworku.

    (A kdyby nešlo, Nette by automaticky přidávalo do všech polí v $_POST prvek ‘!!!’ => ‘security bug’ :-)

  6. Davide, článek není srovnáváním Rails a PHP, na to nejsem dostatečně vzdělaný.

    vod konkurence, od jaký, simtě, čemu konkuruješ? ;) Target audience nejsou RoRaři, ale spíš PHPkáři, kteří si myslí, že by se to v tom jejich PHP nikdy stát nemohlo.

    Na popis Nette bych si ale netroufl, takže jsem rád, že jsi popsal, jak to v Nette funguje. Snad svým komentářem pár lidí přesvědčíš, aby nedělali to, co v článku ukazuju a aby místo toho používali nějakej rozumnej framework, takže díky za doplnění!

    RoR řeší bezpečnost Mass Assignmentu taky nezávisle na HTTP požadavku, proto ta moje narážka na dibi. Z tohoto úhlu pohledu je to tedy podobné.

  7. Disclaimer: Tohle je už třetí verze tohoto komentáře. A asi nejpřesnější :-)

    Rozdíl mezi komunitami PHP a Rails je v tom, čemu adent říká soft-skills. Problém je, že rails obsahují velmi handy knihovnu na generování formulářů – tu učí od úplného začátku. Vedle toho umí mass-asigment, a to taky učí od začátku. Ale nevalidují formuláře

    Napíšu to v php ekvivalentu – standartně se učí generovat formulář asi takto:

    $form->generateFrom('user', UsersModel, array('name', 'surname', 'email', 'password'));

    Zjednodušené, ale budiž. V podstatě jde o to, že tomu pošlu abstract classy modelu, pro který chci vygenerovat formulář, dám formuláři jméno user a vyjmenuju pole, která chci vygenerovat. Mnou vyjmenovaná pole takového formuláře mají klíč user[...]

    Tolik jedna věc. Druhá, nezávislá věc je, že učí mass-assigment. Úplně odděleně, bez návaznosti na formuláře.

    Výsledek je, že když si začátečník (a očividně i pro-user!) dělá aplikaci, tak si pohodlně nageneruje form, a díky struktuře _POST pole pak udělá:

    $user->updateFrom($_POST['user'])->save();

    Tohle bych klidně mohl udělat v Nette takto:

    $user->updateFrom($form->getValues())->save();

    a nic by se nestalo. Všechno by bylo maximálně v pohodě, bez jediného problému. Tak kde je sakra rozdíl?

    Rozdíl je v tom, že v nette $form->getValues(); vrací jenom a pouze ty pole, která jsem si nadefinoval.

    Ano, Rails vůbec nevaliduje formulářová data. On na to totiž nemá knihovny – to co generuje formuláře je šablonový helper – to znamená, že je to de-facto latte tag, twig tag, nazvěte to jak chcete. Není to žádná abstrakce formulářů – je to jenom metoda, která vezme vstupní data a vygeneruje plain-html. Co si s tím programátor pak udělá je na něm.

    Rozdíl mezi Rails a PHP Frameworky je ten, že php frameworky všeobecně nemají takový helper. Mají implementované formuláře včetně ověření (nette), nebo mají mass-assigment (ale narozdíl od rails secure-by-default) a nebo nemají ani jedno.

    Při rychlém pohledu do YII se začínám bát – mají mass-assigment a mají helpery na formuláře, nicméně ne v takovém rozsahu jako Rails. V momentě, kdy budou mít takový helper, pán bůh s námi.

    refs:

    http://guides.rubyonrails.org/form_helpers.html (šablonové form helpery) http://guides.rubyonrails.org/security.html (ani zmínka o form-injection)

    tl;dr Pokud používám mass-assigment, pak nikdy, nikdy, nesmím připustit, aby přes to bylo možné nastavit nebezpečná data – jako id a všeobecně relace a konfigurační hodnoty. už jenom kvůli tomu, že pak nějaký programátor, kolega, který dohání termín, tam může seknout bezpečnostní díru – tím, že tam pošle celý POST. Nazval bych to form-injection. A bojím se, že PHP frameworky, kromě nette, do tohoto směřují také.

  8. Michale, tvůj tvít „PHPkáři, nesmějte se Railsařům, váš kód je taky plnej bezpečnostních chyb” si nedokážu vysvětlit jinak, než jako míchání jazyka s frameworkem.

    Nicméně zcela souhlasím, že vysmívání zcela není na místě.

  9. Jako ex-editor Zdrojáku připomenu, že inkriminovanou Pavlovu větu o PHP jsem v článku nechal, protože na Zdrojáku měla svou funkci. Tam totiž u kdejakého článku o PHP nastoupil dav chytráků s komentáři jako “Nojo, PHP bastliči, bezpečnosti nerozuměj’, to PHPko je samá díra, tohle by se v Rails stát nemohlo”. Tam tohle nevyznělo jako útok na Rails, ale jako odpověď na útoky proti PHP. To jen pro kontext.

  10. Pingback: Bezpečnost je míra, ne vlastnost | DevBlog

  11. Pavel Ptáček: S tím, že v rails guides není ani zmínka o form injection bych si dovolil nesouhlasit. Kapitola 6 je přímo o mass assignment a jak zabránit nastavení některých polí databáze ať už whitelistem nebo blacklistem. Jen to není na úrovni formulářů, ale modelů. http://guides.rubyonrails.org/security.html#mass-assignment

    To, že to někteří vývojáři nepoužívají už je druhá věc.

  12. Pingback: Bezpečnost je míra, ne vlastnost | DevBlog

Leave a Reply

Your email address will not be published. Required fields are marked *

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax