Vydělávej až 160.000 Kč měsíčně! Akreditované rekvalifikační kurzy s garancí práce od 0 Kč. Více informací.
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

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:

Databázová tabuľka užívateľov - Bezpečnosť webových aplikácií v PHP

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:

prihlasovací formulár - Bezpečnosť webových aplikácií v PHP

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:

Vyplnenie nebezpečné hodnoty - Bezpečnosť webových aplikácií v PHP

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_es­cape_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šechny články v sekci
Bezpečnosť webových aplikácií v PHP
Článek pro vás napsal Martin Konečný (pavelco1998)
Avatar
Uživatelské hodnocení:
Ještě nikdo nehodnotil, buď první!
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. Také vyvýjí novou českou prohlížečovou RPG hru a provozuje osobní web http://www.mkonecny.cz
Aktivity