3. diel - Ako sa brániť proti SQL injection
V predchádzajúcej lekcii, Technika útoku SQL injection , sme sa zoznámili s útokom SQL injection.
V dnešnej lekcii si ukážeme, ako sa pred útokom SQL injection chrániť.
Ako aplikáciu chrániť?
Problém je 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 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()
aPDOStatement::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()
a 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
mysqli_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:
$conn = mysqli_connect("localhost", "root", "password", "db"); $name = mysqli_real_escape_string($conn, $_POST["name"]); $password = mysqli_real_escape_string($conn, hash("SHA512", $_POST["password"] . 'sůůůl')); $idQuery = mysqli_query($conn, " 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=' . mysqli_real_escape_string($conn, $_GET['id'])
Útočník môže do parametra GET 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="' . mysqli_real_escape_string($conn, $_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 dátový typ
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žívame parametrizované otázky, nikdy premenné radšej neošetrujte sami.
Ochrana na strane databázy
Ochranu by sme nemali zanedbať ani na strane databázy, kde by malo byť
správne nastavené oprávnenia užívateľa. Keby sa teda podarilo
útočníkovi odoslať jeho vlastné SQL dotaz, databáza by mu zabránila v
prístupe k tabuľkám. Medzi najdôležitejšie oprávnenie patrí príkazy pre
úpravu tabuliek napríklad DROP
a ALTER
, ale taky
práva pre zobrazenie systémových tabuliek uchovávajúci štruktúru
databázy. Vhodné je tiež použiť databázové pohľady, ktoré uchovávajú
zúžený pohľad tabuľky.
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áver
Každý vstup od užívateľa znamená pre naš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, Útok CSRF (Cross Site Request Forgery) a ako sa brániť , sa zoznámime s útokom Cross Site Request Forgery a uvedieme si spôsoby ako sa pred týmto typom útoku brániť.