Obrana proti útoku SQL injection v PHP

PHP Bezpečnost Obrana proti útoku SQL injection v PHP

SQL injection je častá bezpečnostní trhlina mnoha webů. Při tomto typu útoku útočník upraví SQL dotaz ve svůj prospěch. Nejčastěji se používá u dotazů typu SELECT, UPDATE, INSERT a podmínky WHERE. Ukážeme si, jak lze útok provést a jak se mu bránit. V příkladech využijeme databáze MySQL.

Příklad

Ukážeme si příklad s akcí, která je skoro na každém webu - přihlášení uživatele.

Mějme primitivní tabulku s uživateli:

Databázová tabulka uživatelů

Vytvoříme si formulář, do kterého uživatel zadá své přihlašovací jméno a heslo:

<form method="post">
        <table>
                <tr><th><label for="name">Jméno</th></tr>
                <tr><td><input type="text" name="name" id="name" /></td></tr>

                <tr><th><label for="password">Heslo</th></tr>
                <tr><td><input type="password" name="password" id="password" /></td></tr>

                <tr><td><input type="submit" value="Přihlásit se" /></td></tr>
        </table>
</form>

Pro představu může vypadat třeba takto:

Přihlašovací formulář

Při zpracování v PHP potom vložíme hodnoty z $_POST přímo do SQL kódu databázového dotazu pro výběr uživatele. Zpracování může vypadat následovně:

session_start();
$pdo = new PDO("přihlašovací údaje");

$errors = array();
if ($_POST) {
   if (empty($_POST["name"])) {
        $errors[] = "Nebylo vyplněno jméno.";
   }
   if (empty($_POST["password"])) {
        $errors[] = "Nebylo vyplněno heslo.";
   }

   if (empty($errors)) {
        $name = $_POST["name"];
        $password = hash("SHA512", $_POST["password"] . 'sůůůl');
        // Dotaz níže obsahuje nebezpečnou SQL injekci
        $idQuery = $pdo->query("
                SELECT `id`
                FROM `user`
                WHERE `name` = '{$name}' AND `password` = '{$password}'
                LIMIT 1
        ");
        $id = $idQuery->fetchColumn();

        if ($id !== FALSE) {
                $_SESSION["userId"] = $id;
                header("location:account.php");
                exit;
        } else {
                $errors[] = "Bylo zadáno špatné jméno nebo heslo.";
        }
   }
}

Nepoužíváte PDO, ale staré funkce mysql_*? Pak by vyhledání ID vypadalo takto:

// Dotaz níže obsahuje nebezpečnou SQL injekci
$idQuery = mysql_query("
        SELECT `id`
        FROM `user`
        WHERE `name` = '{$name}' AND `password` = '{$password}'
        LIMIT 1
");
$id = mysql_result($idQuery, 0);

Dáme tento kód do ostrého provozu. Přihlášení funguje - my jsme šťastní, uživatelé jsou šťastní. Bohužel jsou v tomto případě šťastní i útočníci.

SQL injection

MySQL databáze má určité speciální znaky. Pro tento případ jsou pro nás důležité tyto:

  • apostrof / uvozovky - ohraničení řetězce
  • pomlčka - dvě pomlčky znamenají komentář (jako např. v PHP dvě lomítka)

Když do našeho formuláře vyplníme například pavelco1998 a tajne_heslo, pak se pošle tento SQL dotaz:

SELECT `id`
FROM `user`
WHERE `name` = 'pavelco1998' AND `password` = '19c44a96a09dc0088f88d...'
LIMIT 1

Nebezpečí nastává v případě, kdy uživatel (jakožto útočník) zadá do jména tento řetězec:

' OR admin = 1--

Do databáze se poté pošle útočníkem upravený SQL dotaz:

SELECT `id`
FROM `user`
WHERE `name` = '' OR admin = 1-- AND `password` = '19c44a96a09dc0088f88d...'
LIMIT 1

Ukázka vstupu:

Vyplnění nebezpečné hodnoty

Všimněte si, že díky zadanému apostrofu se ukončí řetězec u sloupce name a za ním se vloží podmínka OR admin = 1. Díky dvěma pomlčkám se zbytek dotazu považuje za komentář.

Takže pokud napíšeme upravený dotaz slovy, bude vypadat následovně:

Vyber ID z tabulky user, kde jméno se rovná prázdnému řetězci nebo admin se rovná jedné. Zbytek dotazu ignoruj.

Dotaz samozřejmě vybere administrátora, za kterého aplikace útočníka následně přihlásí.

Pokud by chtěl útočník čistě jen někomu ukradnout účet, stačilo by do textového pole zadat následující řetězec: ' OR id = 153-- (či jakékoliv jiné číslo).

Je krádež účtu málo? Dobře, smažeme všechny uživatele:

'; TRUNCATE TABLE user;--

Pro ukázku by se poslal tento dotaz:

SELECT `id`
FROM `user`
WHERE `name` = ''; TRUNCATE TABLE user;-- AND `password` = '19c44a96a09dc0088f88d...'
LIMIT 1

K tomuto útoku již musí web používat ovladač, který podporuje více dotazů v jedné query. Podobně můžeme vymazat msíto tabulky rovnou celou databázi.

Útok lze aplikovat i u jiných dotazů, např. u INSERT:

$pdo->query("
        INSERT INTO `user` (`name`, `password`) VALUES ('$name', '$password')
");

Při zadání řetězce

', ''); TRUNCATE TABLE user;--

Jako jméno uživatele by se vytvořil tento dotaz:

INSERT INTO `user` (`name`, `password`) VALUES ('', ''); TRUNCATE TABLE user;--, 'nejake_heslo')

Někdo by možná argumentoval, že přece návštěvník nezná název naší databáze, tabulky, sloupců atd. Jenže 90% webů má tabulku s uživateli pojmenovanou buď users, uzivatele nebo maximálně user a uzivatel. Útočník má velmi vysokou šanci, že se strefí. Představte si, že uděláte SQL injekci v seriózní aplikaci, kterou si klienti platí. Nejspíše by nebyli spokojeni, kdyby někdo smazal všechna jejich data.

Jak aplikaci chránit?

Problém je samozřejmě v tom, že vkládáme přímo proměnnou do SQL dotazu. A je jedno, jestli je přímo od uživatele nebo ne, vždy je riziko, že může dotaz nějakým způsobem rozbít.

Zastaralý způsob ochrany je ošetřování proměnných (tzv. escapování), které ovšem v několika případech selhává. Jediná správná ochrana proti tomuto útoku je nevkládat proměnné do dotazů vůbec a používat tzv. Prepared Statements (parametrizované dotazy).

Parametrizované dotazy (prepared statements)

SQL dotazy nejprve připravíme a to tak, že místo hodnot napíšeme zástupné znaky. Hodnoty a dotaz předáme databázi úplně odděleně a ona si je tam sama automaticky vloží tak, aby to bylo bezpečné. Automat na rozdíl od lidí nechybuje a my si můžeme být jistí, že se nevystavujeme žádnému riziku.

PDO pro parametrizované dotazy nabízí dvě metody - PDO::prepare() a PDOStatement::e­xecute():

$name = $_POST["name"];
$password = hash("SHA512", $_POST["password"] . 'sůůůl');

$prepared = $pdo->prepare("
        SELECT `id`
        FROM `user`
        WHERE `name` = ? AND `password` = ?
        LIMIT 1
");

// Metoda prepare() vrací instanci třídy PDOStatement
$prepared->execute(array($name, $password));

Místo proměnných použijeme v dotazu otazníky. Proměnné předáme později najednou v poli. Zástupné znaky můžeme i pojmenovat:

$name = $_POST["name"];
$password = hash("SHA512", $_POST["password"]);

$prepared = $pdo->prepare("
        SELECT `id`
        FROM `user`
        WHERE `name` = :name AND `password` = :password
        LIMIT 1
");

$prepared->execute(array(
        ":name" => $name,
        ":password" => $password
));

Pojmenované zástupné znaky se mohou hodit jak pro přehlednost dotazu, tak i v případě, že jednu hodnotu chceme použít vícekrát (nemusíme psát tolik otazníků, kolikrát chceme hodnotu použít).

Některé databázové nadstavby (wrappery) umožňují zápis prepare() - execute() zkrátit. Naše tři metody pro získání ID uživatele bychom mohli vyměnit za jednu:

$id = $db->fetchColumn("
        SELECT `id`
        FROM `user`
        WHERE `name` = ? AND `password` = ?
        LIMIT 1
", $name, $password);

První parametr je SQL dotaz, ostatní jsou parametry, které se předají metodě PDOStatement::e­xecute().

Manuální escapování

Druhým, zastaralým a nebezpečným způsobem obrany proti SQL injekci je proměnné ručně ošetřovat (tzv. escapovat). Tento způsob nepoužívejte, ukážeme si ho jen pro úplnost.

Každá databáze má trochu jinou strukturu SQL dotazu, proto má také jiný algoritmus pro ošetření řetězce. Databáze MySQL pro to má funkci s názvem mysql_real_es­cape_string(). Funkce předsadí nebezpečné znaky jako uvozovky zpětným lomítkem, databáze je potom bere jako text a ne jako část dotazu.

$name = mysql_real_escape_string($_POST["name"]);
$password = mysql_real_escape_string((hash("SHA512", $_POST["password"] . 'sůůůl'));

$idQuery = mysql_query("
        SELECT `id`
        FROM `user`
        WHERE `name` = '{$name}' AND `password` = '{$password}'
        LIMIT 1
");

PDO ovladač má pro tyto účely metodu quote():

$name = $pdo->quote($_POST["name"]);
$password = $pdo->quote(hash("SHA512", $_POST["password"] . 'sůůůl'));

$idQuery = $pdo->query("
        SELECT `id`
        FROM `user`
        WHERE `name` = '{$name}' AND `password` = '{$password}'
        LIMIT 1
");

Jsme tedy zabezpečení? Ne, je to jen iluze. Představte si tento dotaz:

'delete * from user where id=' . mysql_real_escape_string($_GET['id'])

Útočník může do getu zadat řetězec:

1 OR 1=1

A hle, nejsou v něm žádné uvozovky ani jiné škodlivé znaky. Escapovací funkce tedy neprovede nic a útočník stejně vymaže všechny uživatele místo jednoho. Jak se tomu bránit? Zkusme dát hodnotu do uvozovek, i když je to číslo:

'delete * from user where id="' . mysql_real_escape_string($_GET['id']) . '"'

Dotaz funguje, databáze se s číslem zadaným jako text v tomto případě popere. Když bychom takto však zadali číslo v klauzuli LIMIT, máme problém. Jediné řešení je přetypovat parametry na int:

'select * from user LIMIT ' . (int)($_GET['id'])

Musíme přemýšlet nad typem dat a podle toho ručně ošetřovat. Máme spoustu prostoru k tomu, abychom vytvořili nějakou bezpečnostní chybu. Proto vždy používejte parametrizované dotazy, nikdy proměnné neošetřujte sami.

Databázové knihovny

Pro PHP existují knihovny, které dotazy automaticky generují pomocí volání metod. Takové knihovny v nitru obvykle používají parametrizované dotazy. Rozdíl je v tom, že sestaví SQL dotaz přesně podle druhu databáze. Pokud bychom dotaz psali ručně, museli bychom ho po změně databáze upravit (= práce navíc). Například ve frameworku Nette by šlo dotaz sestavit pomocí zřetězení metod:

$id = $db->table("user")->where(array("name" => $name, "password" => $password))->fetch()->id;

Závěrem

Každý vstup od uživatele znamená pro vaši aplikaci potenciální nebezpečí. Nikdy nevkládáme proměnné do SQL dotazu, jinak se vystavujeme bezpečnostnímu riziku. V SQL dotazech se smí vyskytovat pouze zástupné znaky. Proměnné předáme až ve druhém kroku a odděleně.


 

  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 (18 hlasů) :
4.888894.888894.888894.888894.88889


 


Miniatura
Všechny články v sekci
Bezpečnost webových aplikací v PHP

 

 

Komentáře

Avatar
FastNode
Redaktor
Avatar
FastNode:

"návštěvník nezná název naší databáze" -> k tomu existuje database() / např SELECT database() /. Ještě můžu použít information_schema, kde se dají lehce zjistit i názvy tabulek i jednotlivých sloupců myslím... / SELECT SCHEMA_NAME FROM information_schema.SCHEMATA /

Pěkný článek.

Editováno 13.4.2014 21:36
 
Odpovědět 13.4.2014 21:34
Avatar
asanos
Člen
Avatar
Odpovídá na FastNode
asanos:

Před tím by bylo ještě vhodné použít SELECT version() Pokud se jedna o MySQL s verzí menší než 5 -> information_schem nebudeš moci použít a budeš muset pouze hádat = Blind SQL Injection ;)
_____________­________________________­_____________
Ještě je možný způsob obrany s pomocí IDS:
Při použití IDS například zablokujeme použití apostrofů, mezer, nebo různých příkazů. Ale to je hodně ošidné a většinou to jde stejně jednoduše obejít, je to spíš obrana proti laikům, než hackerovi... ;)
např.:

  • Místo mezery dám + nebo víceřádkový komentář /(hvězdička)(hvěz­dička)/. :D
  • Místo apostrofů použijeme funkci char() nebo hexa zápis.

_____________­________________________­_____________
Ještě bych dodal, že přes SQLIA jde udělat i DOS útok, nebo kombinovat s XSS. :D
Tudíž bych tento typ útoku nepodceňoval.
_____________­________________________­_____________
Naprosto nejlepší obrana je přes Prepared Statements(Při­pravené dotazy) V php nejlépe za pomocí PDO + dodat, že se mají k sobě data spojit až v databázi.
_____________­________________________­_____________
Možná s tím označením jako parametrizované dotazy máš pravdu, ale častěji se setkávám s označením jako připravené, nebo předpřipravené dotazy... Jedná se pouze o překlad.

Jinak opravdu pěkný článek, vše podstatné jsi vystihl.

Editováno 15.4.2014 21:22
Odpovědět  +1 15.4.2014 21:20
Na světě je 10 typů lidí. Ti, kteří rozumí binárce a ti co nerozumí.
Avatar
asanos
Člen
Avatar
asanos:

Ohledně tohohle:

'; TRUNCATE TABLE user;--

Zkoušel jsi to?
__________________
Vím, že mysql_query() nepodporuje řetězení dotazů, čili tam by to nešlo, ale jak je na tom PDO::query?

Odpovědět 15.4.2014 21:44
Na světě je 10 typů lidí. Ti, kteří rozumí binárce a ti co nerozumí.
Avatar
FastNode
Redaktor
Avatar
FastNode:

Ještě jsem si rád zvykl na nastavování oprávnění pro každou webovou aplikaci, tzn. jedna databáze = jeden účet. Kromě toho také omezuji povolené příkazy na ty co reálně používám: většinou jen SELECT, INSERT a UPDATE, globální oprávnění pro jednotlivé účty nedávám.

 
Odpovědět 19.4.2014 12:32
Avatar
asanos
Člen
Avatar
Odpovídá na FastNode
asanos:

JJ, omezením počtu příkazů SELECT zabraňuješ útoku Brute force na přihlašovací formulář.
A ještě použít nějakou pomalou hashovací funkci například Blowfish, pomocí crypt() + salt $2y$, se sílou větší jak 10, nejen pro zesílení, ale především pro "zpomalení".

Odpovědět 19.4.2014 17:06
Na světě je 10 typů lidí. Ti, kteří rozumí binárce a ti co nerozumí.
Avatar
TrollKill
Člen
Avatar
TrollKill:

Dobry clanok :D

 
Odpovědět 15.9.2014 13:27
Avatar
Patrik Neumann:

Ahoj, zkouším to mysqli_real_es­cape_string, ale vrací to prázdný řetězec, co s tím?

Odpovědět 31. července 14:10
Nic není nemožné, proto se snažím dál.
Avatar
Odpovědět 31. července 14:33
Nic není nemožné, proto se snažím dál.
Avatar
mkub
Redaktor
Avatar
Odpovídá na Patrik Neumann
mkub:

este lepsie by hadam bolo PDO namiesto mysqli ;)

a keby si rozisal svoj problem a napisal riesenie problemu by tiez nezaskodilo pre inych, ktorym sa vyskytne podobny pripad...

 
Odpovědět 31. července 16:52
Avatar
Odpovídá na mkub
Patrik Neumann:

špatný kód:

mysqli_real_escape_string(string $pole)

řešení:

mysqli_real_escape_string(string $connection, string $pole)
Odpovědět 31. července 20:07
Nic není nemožné, proto se snažím dál.
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 10.