SQL injection je technika napadení databázové vrstvy programu vsunutím (odtud „injection“) kódu přes neošetřený vstup a vykonání vlastního pozměňujícího poškozujícího SQL příkazu (dotazu DELETE, UPDATE, ALTER atp.). Toto nezamýšlené neošetřené chování vzniká při propojení aplikační vrstvy s databázovou vrstvou (téměř vždy se totiž jedná o dva různé programy).
V klasickém případě se jedná o útok na internetové stránky prováděný přes neošetřený formulář, manipulací s URL nebo třeba i podstrčením zákeřně upravené cookie. Na internetu je však stále velké množství webů, spravovaných převážně nezkušenými programátory, kteří o této technice útoku vůbec nevědí a tuto kritickou chybu opomíjejí.
Blind SQL injection se používá, pokud je webová aplikace náchylná k SQL injection útoku, ale výsledek útoku se útočníkovi nezobrazí. Takto zranitelná stránka nemusí být jediná, která zobrazuje data, ale zobrazí se rozdílně v závislosti na výsledku logického výrazu vloženého do SQL dotazu na databázi. Tento typ útoku může být časově náročný, protože každý nový výraz musí být vytvořen pro každý odhalený bit. Existuje několik nástrojů, které pomohou automatizovat tyto útoky, jakmile byla zjištěna lokace zranitelnosti a cílová informace byla zjištěna.
Mějme program odesílající dotaz do databáze:
statement := "SELECT * FROM uzivatele WHERE jmeno = '" + zadaneJmeno + "';"
Pokud však uživatel zadá jako jméno například:
a' or 'b'='b
aplikační program dotaz doplní a odešle databázi ve formě:
statement := "SELECT * FROM uzivatele WHERE jmeno = 'a' or 'b'='b';"
což může zapříčinit přemostění autentizační procedury, protože 'b' = 'b' je vždy pravda, tudíž klientská (zobrazovací) vrstva aplikace vypíše všechny uživatele (nejen s jménem 'a'), pokud se jedná o stránku záznamů, tj. ne stránku vlastností jednoho záznamu se jménem 'a'. Pro SQL injection se samozřejmě dají použít všechny dostupné příkazy. Pokud by tedy útočník v předešlém příkladě jako jméno zadal:
a';DROP TABLE uzivatele; --
vypadal by dotaz při odeslání serveru jako:
statement := "SELECT * FROM uzivatele WHERE jmeno = 'a';DROP TABLE uzivatele; --';"
čímž by smazal celou tabulku uživatelů, pokud dotaz proběhně pod oprávněním uživatele s právem mazat databázové objekty (viz dále Obrana na straně databáze). Poslední apostrof se pomocí sekvence dvou pomlček stane poznámkou a nemá žádný vliv. Podobných průniků je samozřejmě celá řada. Dokonce díky klauzulím UNION a JOIN nejsme ani vázáni na tabulku předepsanou v části FROM a můžeme vypisovat data odkudkoliv z databáze.
Přímočarý, i když k chybám náchylný způsob, jak zabránit SQL útokům, je takzvané „Escapování“ znaků, které mají speciální význam v SQL. Manuál pro SQL DBMS vysvětluje, které znaky mají speciální význam, což povoluje vytvoření blacklistu znaků, které potřebují přeložit. Například každý výskyt apostrofu (') v parametru musí být nahrazen dvěma apostrofy pro vytvoření úplného validního řetězce. Například v PHP je při escapování zvykem používat funkcí mysql_real_escape_string() ještě před odesláním dotazu k provedení v databázi.
$query = $sql->prepare(
"SELECT * FROM uzivatele WHERE jmeno = "
. $sql->quote($zadaneJmeno)
);
Příklad využívá moderní knihovnu MySQLi:
$link = @mysqli_connect(db_hostname, db_username, db_password, db_name); //otevření nového připojení do MySQL
$val1 = mysqli_real_escape_string($link, $_POST["va1"]); //escapuje nedovolené znaky ze superglob. proměnné
$val2 = mysqli_real_escape_string($link, $_GET["va1"]); //escapuje nedovolené znaky ze superglob. proměnné
$query = sprintf("SELECT * FROM `test` WHERE value1='%s' AND value2='%s'",
$val1,
$val2); //vytvoří dotaz do databáze s již ošetřenými hodnotami
$result = mysqli_query($link, $query); //vrací výsledek dotazu
/*
Zde můžeme využívat výsledky z databáze uložené v $result např.:
while($row = mysqli_fetch_object($result)) {
echo $row->value3;
...
}
*/
mysqli_close($link); //uzavřeme spojení s databází
Takto může vypadat ošetřený kód proti sql injection. Vstupní proměnné jsou zde ošetřeny pomocí funkce mysqli_real_escape_string(mysqli $link, string $escapestr), která přidá zpětné lomítko následujícím znakům: \x00, \r, \n, \, ', , a \x1a. Tato funkce by měla být použita pro každou proměnnou předávanou dotazu. Ovšem existují i výjimky u kterých je zbytečné tuto funkci použit, např. pokud se jedná o proměnnou obsahující pouze celé číslo (int), zde se doporučuje použít v PHP intval(). Níže je ukázka staršího způsobu ošetření kódu proti SQL injection, tzv. escapování proměnných:
<?php
$mysqli = new mysqli("localhost", "redakcni_system", "tajne-heslo", "redakcni_system");
if ($mysqli->character_set_name()!="utf8mb4") { $mysqli->set_charset("utf8mb4"); }
$ucet = $_POST['ucet'];
$ucet = $mysqli->real_escape_string($ucet); // starší obrana proti SQL injection
$dotaz = "select * from uzivatele where ucet=\"".$ucet."\"";
$vysledek = $mysqli->query($dotaz);
$mysqli->close();
?>
Nyní si ukážeme názornou ukázku (vizualizace) SQL injection, zadání přihlašovacího jména a hesla. Při chybějícím řádku okomentovaném jako "starší obrana proti SQL injection" se provede SQL injektáž a útočník i bez zadání hesla získá všechny údaje z tabulky uzivatelé. Místo očekávaného dotazu
select * from uzivatele where ucet="admin";
se provede již daty upravený SQL dotaz útočníka:
select * from uzivatele where ucet="" OR "1"="1";
Získané údaje útočníka získané úspěšným útokem, SQL injection, z přihlašovacího formuláře:
select * from uzivatele where ucet="" OR "1"="1";
+-------------+--------------+----------+-------+--------------------------------------------------------------+---------------+
| uzivatel_id | ucet | Prijmeni | jmeno | heslo | funkce |
+-------------+--------------+----------+-------+--------------------------------------------------------------+---------------+
| 1 | admin | Novák | Josef | $2y$10$yC9GNxDIZkod9FYvmZWx4ucBWmcISVo5HPBkBVs30aoLtLyltK9pW | správce |
| 2 | pjotr | Hladiš | Petr | $2y$10$2Lkshc/udDrQzk1eP8jcIuadhIFVsLffQDS6hh93.ttiOvukc92Qy | šéfredaktor |
| 3 | Jana | Malá | Jana | $2y$10$mWBfBo.o.aWI16GdAgMgb.0C3y/U3JpBMAZ.SsxESVxI9DLZtpLsG | redaktor |
| 4 | prispevatel1 | Novotný | Jan | $2y$10$Ww25MJ8b5KfS888gyRf0PukEEQTvD0kYtr.ozE52PiaWif8RSCpFa | přispěvatel |
+-------------+--------------+----------+-------+--------------------------------------------------------------+---------------+
4 rows in set (0,001 sec)
Za nejmodernější obranu proti SQL injection odpovídající přelomu roku 2022/2023 se považuje oddělené předávání dotazu a jeho parametrů databázovému serveru, tedy parametrizované SQL dotazy. Pro příklad mějme databázi "redakcni_system" s tabulkou "uzivatele":
MariaDB [redakcni_system]> show tables;
+---------------------------+
| Tables_in_redakcni_system |
+---------------------------+
| uzivatele |
+---------------------------+
1 row in set (0,001 sec)
MariaDB [redakcni_system]> describe uzivatele;
+-------------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+------------------+------+-----+---------+----------------+
| uzivatel_id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| ucet | varchar(25) | YES | UNI | NULL | |
| Prijmeni | varchar(25) | YES | | NULL | |
| jmeno | varchar(25) | YES | | NULL | |
| heslo | varchar(128) | YES | | NULL | |
| funkce | varchar(20) | YES | | NULL | |
+-------------+------------------+------+-----+---------+----------------+
6 rows in set (0,002 sec)
MariaDB [redakcni_system]> select * from uzivatele;
+-------------+--------------+----------+--------+--------------------------------------------------------------+---------------+
| uzivatel_id | ucet | Prijmeni | jmeno | heslo | funkce |
+-------------+--------------+----------+--------+--------------------------------------------------------------+---------------+
| 1 | admin | Novák | Josef | $2y$10$yC9GNxDIZkod9FYvmZWx4ucBWmcISVo5HPBkBVs30aoLtLyltK9pW | správce |
| 2 | pjotr | Hladiš | Petr | $2y$10$2Lkshc/udDrQzk1eP8jcIuadhIFVsLffQDS6hh93.ttiOvukc92Qy | šéfredaktor |
| 3 | Jana | Malá | Jana | $2y$10$mWBfBo.o.aWI16GdAgMgb.0C3y/U3JpBMAZ.SsxESVxI9DLZtpLsG | redaktor |
| 4 | prispevatel1 | Novotný | Jan | $2y$10$Ww25MJ8b5KfS888gyRf0PukEEQTvD0kYtr.ozE52PiaWif8RSCpFa | přispěvatel |
+-------------+--------------+----------+--------+--------------------------------------------------------------+---------------+
Nyní pro autorizaci (přihlášení) je nejlepší provést parametrizovaný dotaz, kdy si nejprve ověříme, zda počet vrácených záznamů je pouze jeden (nebo žádný). Nikdy není správné, pokud by byl počet > 1. Níže je uveden příklad parametrizovaného dotazu v PHP s rozhraním MySQLi a nad databází MariaDB (dříve MySQL):
$mysqli = new mysqli("localhost", "redakcni_system", "tajne-heslo", "redakcni_system");
if ($mysqli->character_set_name()!="utf8mb4") { $mysqli->set_charset("utf8mb4"); }
$delka=15; $ucet = mb_substr(trim($_POST['ucet']),0,$delka,"utf-8"); $ucet=strip_tags($ucet);
$ucet = str_replace(array("'",">","<",'"'), array("","","",""), $ucet);
// =================== parametrizovaný dotaz ===============
$dotaz = "select * from uzivatele where ucet=?";
$typy = "s";
$parametry = array($ucet);
if ($stmt = $mysqli->prepare($dotaz)) {
$stmt->bind_param($typy, ...$parametry);
$stmt->execute();
$vysledek = $stmt->get_result();
$stmt->close();
$poczaznamu = $vysledek->num_rows;
while($zaznam = $vysledek->fetch_assoc()) {
echo $zaznam['ucet']." - ".$zaznam['jmeno']." ".$zaznam['Prijmeni']." - ".$zaznam['funkce']."<br />";
}
}
$mysqli->close();
Jako typy parametrů (zde proměnná $typy) se používá:
Proč raději vždy používat pouze pole v
$stmt->bind_param($typy, ...$parametry)
je patrné z ukázky pro úpravu záznamu(ů), která je (ta ukázka) s parametrizovaným dotazem odolná proti SQL injection:
$mysqli = new mysqli("localhost", "redakcni_system", "tajne-heslo", "redakcni_system");
if ($mysqli->character_set_name()!="utf8mb4") { $mysqli->set_charset("utf8mb4"); }
$uid = intval($_POST['uid']);
$delka=15; $ucet = mb_substr(trim($_POST['ucet']),0,$delka,"utf-8"); $ucet=strip_tags($ucet);
$delka=50; $heslo = mb_substr(trim($_POST['heslo']),0,$delka,"utf-8"); $heslo = password_hash($heslo, PASSWORD_DEFAULT);
$delka=25; $prijmeni = mb_substr(trim($_POST['prijmeni']),0,$delka,"utf-8"); $prijmeni=strip_tags($prijmeni);
$delka=25; $jmeno = mb_substr(trim($_POST['jmeno']),0,$delka,"utf-8");
$jmeno=strip_tags($jmeno); $jmeno = str_replace(array("'",">","<",'"'), array("","","",""), $jmeno);
$dotaz = "UPDATE uzivatele SET ucet = ?, heslo = ?, Prijmeni = ?, jmeno = ? WHERE uzivatel_id = ?";
$typy = "ssssi";
$parametry = array($ucet,$heslo,$prijmeni,$jmeno,$uid);
if ($stmt = $mysqli->prepare($dotaz)) {
$stmt->bind_param($typy, ...$parametry);
$stmt->execute();
echo $stmt->affected_rows;
$stmt->close();
}
$mysqli->close();
$stmt = oci_parse($connOci, 'SELECT * FROM LOGIN WHERE jmeno = :login');
oci_bind_by_name($stmt, ':login', $_POST['login']);
oci_execute($stmt);
Existuje však mnoho funkcí pro různé typy databází v PHP, jako například pg_escape_string() for PostgreSQL. Další funkce, které slouží k ochraně databází, které neobsahují funkce pro escapování v SQL. Funkce se nazývá addslashes(string $str). Navrací string se zpětnými lomítky před znaky, které musí být v dotazech na databázi v uvozovkách. Mezi tyto znaky patří jednoduchá uvozovka ('), dvojitá uvozovka (“), zpětné lomítko (\) a NULL. Běžné předávání escapovaných řetězců SQL databázi je náchylné k chybám, protože je snadné zapomenout daný řetězec escapovat. Vytvoření transparentní vrstvy k ošetření dat ze vstupu může snížit náchylnost k chybám, dokonce ji eliminovat úplně.
Obecně řečeno, spolu s tímto je vhodné aplikaci otestovat pro všechny možnosti uživatelského vstupu. Ve vývojové verzi se doporučuje mít nastavenu nízkou úroveň vypisování chybových hlášek a varování (abychom si jich všimli a opravili je). V ostré verzi je naopak zvykem vypisování chybových hlášek co nejvíc potlačit (mohly by poskytnout útočníkovi dodatečné informace).
V databázi můžeme útoku zabránit (nebo ho přinejmenším extrémně ztížit) vhodným nastavením oprávnění uživatele, se kterými bude program přistupovat k úložišti, k čemuž je možné využít i tzv. VIEW (zúžený, nebo naopak syntetizovaný POHLED na záznamy tabulek v databázi, který je navíc výkonnější). Málokdy je třeba přímo z aplikační vrstvy mazat (ev. upravovat atp.) tabulky anebo dokonce celé databáze, takže je vhodné pro aplikací využívaného databázového uživatele (pokud možno ne správce databáze) zakázat jak relevantní SQL příkazy (DROP, ALTER atp.), tak přístup k systémovým tabulkám daného databázového serveru.
Mezi (poměrně úzkou) komunitou programátorů, bezpečnostních analytiků a databázových návrhářů se v posledních několika měsících dokonce rozšířil humor, narážející na některé tyto útoky, mezi jinými i SQL injection.[5][6][7]
V tomto článku byl použit překlad textu z článku SQL injection na anglické Wikipedii.