Obrana proti útoku SQL injection v PHP
SQL injection je častá bezpečnostná trhlina mnohých webov. Pri tomto type útoku útočník upraví SQL dotaz vo svoj prospech. Najčastejšie sa používa u otázok typu SELECT, UPDATE, INSERT a podmienky WHERE. Ukážeme si, ako možno útok vykonať a ako sa mu brániť. V príkladoch využijeme databázy MySQL.
Príklad
Ukážeme si príklad s akciou, ktorá je skoro na každom webe - prihlásenie užívateľa.
Majme primitívne tabuľku s používateľmi:
Vytvoríme si formulár, do ktorého používateľ zadá svoje prihlasovacie meno 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>
Pre predstavu môže vyzerať napríklad takto:
Pri spracovaní v PHP potom vložíme hodnoty z $ _POST priamo do SQL kódu databázového dopytu pre výber užívateľa. Spracovanie môže vyzerať nasledovne:
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žívate PDO, ale staré funkcie mysql_ *? Potom by vyhľadanie ID vyzeralo 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 ostrej prevádzky. Prihlásenie funguje - my sme šťastní, užívatelia sú šťastní. Bohužiaľ sú v tomto prípade šťastní aj útočníci.
SQL injection
MySQL databázy má určité špeciálne znaky. Pre tento prípad sú pre nás dôležité tieto:
- apostrof / úvodzovky - ohraničenie reťazca
- pomlčka - dve pomlčky znamenajú komentár (ako napr. v PHP dve lomítka)
Keď do nášho formulára vyplníme napríklad pavelco1998 a tajne_heslo, potom sa pošle tento SQL dotaz:
SELECT `id` FROM `user` WHERE `name` = 'pavelco1998' AND `password` = '19c44a96a09dc0088f88d...' LIMIT 1
Nebezpečenstvo nastáva v prípade, keď užívateľ (ako útočník) zadá do mena tento reťazec:
' OR admin = 1--
Do databázy sa potom pošle útočníkom upravený SQL dotaz:
SELECT `id` FROM `user` WHERE `name` = '' OR admin = 1-- AND `password` = '19c44a96a09dc0088f88d...' LIMIT 1
Ukážka vstupu:
Všimnite si, že vďaka zadanému apostrofu sa ukončí reťazec u stĺpce
name
a za ním sa vloží podmienka OR admin = 1. Vďaka dvom
pomlčkám sa zvyšok dopytu považuje za komentár.
Takže ak napíšeme upravený otázku slovami, bude vyzerať nasledovne:
Vyber ID z tabulky user, kde jméno se rovná prázdnému řetězci nebo admin se rovná jedné. Zbytek dotazu ignoruj.
Otázka samozrejme vyberie administrátora, za ktorého aplikácia útočníka následne prihlási.
Ak by chcel útočník čisto len niekomu ukradnúť účet, stačilo by do textového poľa zadať nasledujúci reťazec: 'OR id = 153-- (či akékoľvek iné číslo).
Je krádež účtu málo? Dobre, zmažeme všetkých užívateľov:
'; TRUNCATE TABLE user;--
Pre ukážku by sa poslal tento dotaz:
SELECT `id` FROM `user` WHERE `name` = ''; TRUNCATE TABLE user;-- AND `password` = '19c44a96a09dc0088f88d...' LIMIT 1
K tomuto útoku už musia web používať ovládač, ktorý podporuje viac otázok v jednej query. Podobne môžeme vymazať msíto tabuľky rovno celú databázu.
Útok je možné aplikovať aj u iných otázok, napr. U INSERT:
$pdo->query(" INSERT INTO `user` (`name`, `password`) VALUES ('$name', '$password') ");
Pri zadaní reťazca
', ''); TRUNCATE TABLE user;--
Ako meno používateľa by sa vytvoril tento dotaz:
INSERT INTO `user` (`name`, `password`) VALUES ('', ''); TRUNCATE TABLE user;--, 'nejake_heslo')
Niekto by možno argumentoval, že predsa návštevník nepozná názov našej databázy, tabuľky, stĺpcov atď. Lenže 90% webov má tabuľku s používateľmi pomenovanú buď users, uzivatelia alebo maximálne user a uzivatel. Útočník má veľmi vysokú šancu, že sa trafí. Predstavte si, že urobíte SQL injekciu v serióznej aplikáciu, ktorú si klienti platia. Pravdepodobne by neboli spokojní, keby niekto zmazal všetky ich dáta.
Ako aplikáciu chrániť?
Problém je samozrejme v tom, že vkladáme priamo premennú do SQL dotazu. A je jedno, či je priamo od užívateľa alebo nie, vždy je riziko, že môže dotaz nejakým spôsobom rozbiť.
Zastaralý spôsob ochrany je ošetrovania premenných (tzv. Uvádzacích), ktoré však v niekoľkých prípadoch zlyháva. Jediná správna ochrana proti tomuto útoku je nevkladať premenné do otázok vôbec a používať tzv. Prepared Statements (parametrizované otázky).
Parametrizované otázky (prepared statements)
SQL dotazy najprv pripravíme a to tak, že namiesto hodnôt napíšeme zástupné znaky. Hodnoty a otázka odovzdáme databázu úplne oddelene a ona si ich tam sama automaticky vloží tak, aby to bolo bezpečné. Automat na rozdiel od ľudí neprehrešuje a my si môžeme byť istí, že sa nevystavujeme žiadnemu riziku.
PDO pre parametrizované otázky ponúka dve metódy - PDO :: prepare () a PDOStatement :: execute ():
$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));
Miesto premenných použijeme v dotaze otázniky. Premenné odovzdáme neskôr naraz v poli. Zástupné znaky môžeme aj pomenovať:
$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 ));
Pomenované zástupné znaky sa môžu hodiť ako pre prehľadnosť dopytu, ako aj v prípade, že jednu hodnotu chceme použiť viackrát (nemusíme písať toľko otáznikov, koľkokrát chceme hodnotu použiť).
Niektoré databázové nadstavby (wrappery) umožňujú zápis prepare () - execute () skrátiť. Naše tri metódy pre získanie ID používateľa by sme mohli vymeniť za jednu:
$id = $db->fetchColumn(" SELECT `id` FROM `user` WHERE `name` = ? AND `password` = ? LIMIT 1 ", $name, $password);
Prvý parameter je SQL dotaz, ostatné sú parametre, ktoré sa odovzdajú metóde PDOStatement :: execute ().
Manuálny uvádzacích
Druhým, zastaraným a nebezpečným spôsobom obrany proti SQL injekciu je premennej ručne ošetrovať (tzv. Escapovat). Tento spôsob nepoužívajte, ukážeme si ho len pre úplnosť.
Každá databáza má trochu inú štruktúru SQL dotazu, preto má aj iný algoritmus pre ošetrenie reťazca. Databáza MySQL pre to má funkciu s názvom mysql_real_escape_string (). Funkcia Predsadí nebezpečné znaky ako úvodzovky spätným lomítkom, databázy je potom berie ako text a nie ako časť 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 ovládač má pre tieto účely metódu 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 ");
Sme teda zabezpečenia? Nie, je to len ilúzia. Predstavte si tento dotaz:
'delete * from user where id=' . mysql_real_escape_string($_GET['id'])
Útočník môže do getu zadať reťazec:
1 OR 1=1
A hľa, nie sú v ňom žiadne úvodzovky ani iné škodlivé znaky. Escapovací funkcie teda nevykoná nič a útočník rovnako vymaže všetkých užívateľov namiesto jedného. Ako sa tomu brániť? Skúsme dať hodnotu do úvodzoviek, aj keď je to číslo:
'delete * from user where id="' . mysql_real_escape_string($_GET['id']) . '"'
Otázka funguje, databáza sa s číslom zadaným ako text v tomto prípade pobije. Keď by sme takto však zadali číslo v klauzule LIMIT, máme problém. Jediné riešenie je pretypovať parametre na int:
'select * from user LIMIT ' . (int)($_GET['id'])
Musíme premýšľať nad typom dát a podľa toho ručne ošetrovať. Máme veľa priestoru na to, aby sme vytvorili nejakú bezpečnostnú chybu. Preto vždy používajte parametrizované otázky, nikdy premenné neošetrujte sami.
Databázové knižnice
Pre PHP existujú knižnice, ktoré otázky automaticky generujú pomocou volanie metód. Takéto knižnice v vnútri obvykle používajú parametrizované otázky. Rozdiel je v tom, že zostaví SQL dotaz presne podľa druhu databázy. Ak by sme dotaz písali ručne, museli by sme ho po zmene databázy upraviť (= práce navyše). Napríklad vo frameworku Nette by šlo otázku zostaviť pomocou zreťazenie metód:
$id = $db->table("user")->where(array("name" => $name, "password" => $password))->fetch()->id;
Záverom
Každý vstup od užívateľa znamená pre vašu aplikáciu potenciálne nebezpečenstvo. Nikdy nevkladáme premenné do SQL dotazu, inak sa vystavujeme bezpečnostnému riziku. V SQL otázkach sa smie vyskytovať iba zástupné znaky. Premenné odovzdáme až v druhom kroku a oddelene.
V ďalšej lekcii, Technika útoku SQL injection , sa zoznámime s útokom SQL injection.