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í.
Pouze tento týden sleva až 80 % na e-learning týkající se C a C++. Zároveň využij akce až 80 % zdarma při nákupu e-learningu - více informací.
discount week 80 + hiring

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

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

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

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.

Tento výukový obsah pomáhají rozvíjet následující firmy, které dost možná hledají právě tebe!

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

 

 

Komentáře

Avatar
mkub
Redaktor
Avatar
mkub:31.7.2016 16:52

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.7.2016 16:52
Avatar
Odpovídá na mkub
Patrik Neumann:31.7.2016 20:07

špatný kód:

mysqli_real_escape_string(string $pole)

řešení:

mysqli_real_escape_string(string $connection, string $pole)
Odpovědět
31.7.2016 20:07
Nic není nemožné, proto se snažím dál.
Avatar
Adam Harajda
Člen
Avatar
Adam Harajda:5.1.2017 13:49

A nedalo by sa to ošetriť aj cez povolené znaky? napríklad takto:

if(preg_match('(^[A-Za-z0-9]{4,32}$)',$­_POST['name'])){
/*vstup je správny*/
}
else{
/*vstup je nesprávny*/
}

 
Odpovědět
5.1.2017 13:49
Avatar
Odpovídá na Adam Harajda
Martin Konečný (pavelco1998):5.1.2017 14:10

Je to také možné, ale osobně doporučuji ošetřovat každý vstup. Je lepší ošetřit a nepotřebovat, než potřebovat a mít v aplikaci díru.
Větší jistotu máš, když použiješ knihovny, které escapování řeší za tebe. Viz třeba parametrizované dotazy v PDO, nějaká DB layery ve frameworcích atp.

Odpovědět
5.1.2017 14:10
Aktuálně připravuji browser RPG, FB stránka - https://www.facebook.com/AlteiraCZ
Avatar
posvicmichal
Člen
Avatar
posvicmichal:28.6.2017 18:15

Chci se zeptat, do jaké míry může proti SQL injekci pomoct pojmenovávání sloupců nějakým jiným způsobem. Například místo "id" napsat "111iD222" a podobně. Je mi jasné, že je to v každém případě dost "špinavý postup", ale může pomoct?

 
Odpovědět
28.6.2017 18:15
Tento výukový obsah pomáhají rozvíjet následující firmy, které dost možná hledají právě tebe!
Avatar
Odpovídá na posvicmichal
Martin Konečný (pavelco1998):28.6.2017 18:56

Ahoj,
v první řadě nemá smysl pojmenovávat jinak sloupce kvůli "bezpečnosti", když je jiná a daleko spolehlivější ochrana. Pomoct by to mohlo jedině do doby, dokud by útočník nepoznal, jak se ten daný sloupec jmenuje. Rozhodně tento způsob ochrany nedoporučuji.

Odpovědět
28.6.2017 18:56
Aktuálně připravuji browser RPG, FB stránka - https://www.facebook.com/AlteiraCZ
Avatar
Odpovídá na posvicmichal
Michal Štěpánek:29.6.2017 8:28

Jak píše kolega, divný název sloupce, není ochrana protí případným útočníkům. DB ochráníš jen tak, že budeš psát příkazy tak, aby je nemohl nikdo "zneužít"., tzn. např. používat parametry.

Odpovědět
29.6.2017 8:28
Nikdy neříkej nahlas, že to nejde. Vždycky se totiž najde blbec, který to neví a udělá to...
Avatar
Šimon Rataj
Člen
Avatar
Šimon Rataj:5.6.2018 16:40

Podobně můžeme vymazat mto tabulky rovnou celou databázi.

 
Odpovědět
5.6.2018 16:40
Avatar
Marty
Člen
Avatar
Marty:5.10.2018 1:58

Dokáže ten parametrizovaný dotaz ochránit i před tímhle? V článku jsem nenašel odpověď. Díky.

1 OR 1=1

 
Odpovědět
5.10.2018 1:58
Avatar
Odpovídá na Marty
Martin Konečný (pavelco1998):5.10.2018 2:57

Zdravím,

parametrizované dotazy chrání dotazy před SQL injection, tedy i před uvedeným kódem.

Odpovědět
5.10.2018 2:57
Aktuálně připravuji browser RPG, FB stránka - https://www.facebook.com/AlteiraCZ
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.

Zatím nikdo nevložil komentář - buď první!