Myslíte si, že je vaše databáze SQL neotřesitelná a absolutně bezpečná? Injekce SQL by s tímto tvrzením s velkou pravděpodobností nesouhlasila!
Mluvím o okamžitém kolapsu, protože se nechci pouštět do otřepaných frází o „posilování zabezpečení“ nebo „zabránění neoprávněnému přístupu“. Injekce SQL je trik tak starý, že každý vývojář o něm ví a má znát i způsoby prevence. Stačí však jeden jediný moment nepozornosti a následky mohou být zničující.
Pokud jste s pojmem SQL injection již obeznámeni, můžete přeskočit do druhé poloviny tohoto článku. Pro ty, kteří s webovým vývojem teprve začínají a touží po vyšších pozicích, je ovšem vhodné si tento pojem uvést.
Co je to SQL Injekce?
Klíč k pochopení SQL injection je v samotném názvu: SQL + Injekce. V tomto případě se nejedná o medicínský termín, ale spíše o úmyslné vložení. Dohromady tedy tato slova vyjadřují myšlenku vkládání SQL do webové aplikace.
Vkládání SQL do webové aplikace. . . Hmm . . . Není to ale to, co běžně děláme? Ano, ale rozhodně nechceme, aby naši databázi ovládal útočník. Ukažme si to na příkladu.
Představte si, že vytváříte typický PHP web pro lokální e-shop a rozhodnete se přidat kontaktní formulář:
<form action="record_message.php" method="POST"> <label>Vaše jméno</label> <input type="text" name="name"> <label>Vaše zpráva</label> <textarea name="message" rows="5"></textarea> <input type="submit" value="Odeslat"> </form>
Předpokládejme, že soubor send_message.php
ukládá vše do databáze, aby si majitelé mohli zprávy později přečíst. Mohl by obsahovat kód, jako je tento:
<?php $name = $_POST['name']; $message = $_POST['message']; // kontrola, zda uživatel již má zprávu mysqli_query($conn, "SELECT * from messages where name = $name"); // Další kód
Nejprve zjišťujeme, zda uživatel již nemá nepřečtenou zprávu. Dotaz SELECT * from messages where name = $name
vypadá poměrně jednoduše, že?
CHYBA!
Ve své nevinnosti jsme otevřeli dveře okamžitému zničení databáze. K tomu, aby se tak stalo, musí útočník splnit následující podmínky:
- Aplikace běží na SQL databázi (což je dnes téměř pravidlem).
- Aktuální databázové připojení má v databázi oprávnění „upravit“ a „smazat“.
- Názvy důležitých tabulek lze uhodnout.
Třetí bod znamená, že pokud útočník ví, že provozujete e-shop, je velmi pravděpodobné, že ukládáte data objednávek do tabulky objednávky
. S těmito informacemi stačí, aby útočník zadal toto jako své jméno:
Joe; truncate orders; --
. Podívejme se, jak bude dotaz vypadat, když ho PHP skript spustí:
SELECT * FROM messages WHERE name = Joe; truncate orders; --
První část dotazu obsahuje sice syntaktickou chybu (chybí uvozovky kolem „Joe“), ale středník způsobí, že MySQL interpretuje nový příkaz: truncate orders
. Jediným příkazem je tak historie objednávek pryč!
Nyní, když víte, jak SQL injection funguje, je čas podívat se na způsoby, jak ji zastavit. Dvě podmínky, které musí být splněny pro úspěšnou SQL injekci jsou:
- PHP skript by měl mít oprávnění upravovat nebo mazat data. To je bohužel obvyklé u většiny aplikací a nemůžeme jednoduše nastavit aplikaci do režimu „pouze ke čtení“. I v případě, že odebereme veškerá oprávnění k úpravám, může SQL injection umožnit spouštět dotazy
SELECT
a prohlížet celou databázi včetně citlivých údajů. Zkrátka, omezení přístupů k databázi není řešením a vaše aplikace to stejně potřebuje. - Vstup od uživatele je zpracováván. SQL injection funguje pouze v případě, že akceptujete data od uživatele. Opět platí, že není praktické blokovat veškerý uživatelský vstup z obav před SQL injection.
Jak zabránit SQL Injekci v PHP
Vzhledem k tomu, že databázová připojení, dotazy a uživatelské vstupy jsou nedílnou součástí webové aplikace, jak se tedy můžeme bránit SQL injection? Naštěstí je to poměrně jednoduché a existují dva způsoby: 1) sanitizovat uživatelský vstup a 2) používat připravené příkazy.
Sanitizace uživatelského vstupu
Pokud používáte starší verzi PHP (5.5 nebo nižší, což je stále poměrně časté na sdíleném hostingu), je rozumné projít všechny uživatelské vstupy funkcí mysql_real_escape_string()
. Tato funkce odstraní všechny speciální znaky v řetězci, takže při použití v dotazu ztratí svůj původní význam.
Například, pokud máme řetězec jako já jsem řetězec
, může útočník použít jednoduchou uvozovku ('
) pro manipulaci s databázovým dotazem a způsobit SQL injection. Po aplikaci mysql_real_escape_string()
získáme řetězec I\'m
, kde je zpětné lomítko přidáno pro „escapování“ jednoduché uvozovky. Výsledkem je, že celý řetězec je předán do databáze jako neškodná textová hodnota a nemůže být použit k manipulaci s dotazem.
Tento přístup má jednu nevýhodu: je to skutečně stará technika spojená se staršími způsoby přístupu k databázi v PHP. Od PHP 7 tato funkce již neexistuje, což nás přivádí k dalšímu řešení.
Použití připravených příkazů
Připravené příkazy představují bezpečnější a spolehlivější způsob provádění databázových dotazů. Místo zasílání nezpracovaného dotazu do databáze nejprve databázi sdělíme strukturu dotazu, který budeme zasílat. Právě toto se myslí slovem „příprava“ příkazu. Jakmile je příkaz připraven, předáme informace jako parametrizované vstupy, takže databáze může „vyplnit mezery“ vložením vstupů do struktury dotazu, kterou jsme dříve odeslali. To odstraní jakýkoli speciální význam vstupů a v průběhu celého procesu se s nimi zachází jako s běžnými proměnnými. Takto vypadají připravené příkazy:
<?php $servername = "localhost"; $username = "username"; $password = "password"; $dbname = "myDB"; // Vytvoření spojení $conn = new mysqli($servername, $username, $password, $dbname); // Kontrola spojení if ($conn->connect_error) { die("Spojení selhalo: " . $conn->connect_error); } // příprava a vazba $stmt = $conn->prepare("INSERT INTO MyGuests (firstname, lastname, email) VALUES (?, ?, ?)"); $stmt->bind_param("sss", $firstname, $lastname, $email); // nastavení parametrů a spuštění $firstname = "John"; $lastname = "Doe"; $email = "[email protected]"; $stmt->execute(); $firstname = "Mary"; $lastname = "Moe"; $email = "[email protected]"; $stmt->execute(); $firstname = "Julie"; $lastname = "Dooley"; $email = "[email protected]"; $stmt->execute(); echo "Nové záznamy úspěšně vytvořeny"; $stmt->close(); $conn->close(); ?>
Vím, že tento proces zní složitě, pokud s připravenými příkazy teprve začínáte, ale tento koncept rozhodně stojí za pochopení. Zde je pěkný úvod do tohoto tématu.
Pro ty, kteří jsou obeznámeni s rozšířením PDO v PHP a jeho používáním pro vytváření připravených příkazů, mám malé upozornění.
Pozor při konfiguraci PDO
Při používání PDO pro přístup k databázi se můžeme dostat do falešného pocitu bezpečí. „Používám PDO, tak je to v pořádku. Už se nemusím o nic starat.“ Je pravda, že PDO (nebo MySQLi připravené příkazy) by měly být dostatečné pro prevenci většiny SQL injection útoků, ale je nutné dbát opatrnosti při jeho konfiguraci. Je běžné kopírovat kód z tutoriálů nebo z dřívějších projektů a automaticky pokračovat. Toto nastavení ale může veškeré úsilí zmařit:
$dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
Toto nastavení říká PDO, aby emulovalo připravené příkazy, místo aby skutečně využívalo funkci připravených příkazů databáze. V důsledku toho PHP zasílá do databáze obyčejné řetězce dotazů, i když se zdá, že váš kód vytváří připravené příkazy a nastavuje parametry. Jinými slovy, jste stejně zranitelní vůči SQL injection jako dříve.
Řešení je jednoduché: Ujistěte se, že je tato emulace nastavena na hodnotu false
.
$dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
Nyní je PHP skript nucen používat připravené příkazy na úrovni databáze, což zabraňuje veškerým formám SQL injection.
Ochrana pomocí WAF
Věděli jste, že webové aplikace můžete chránit před SQL injection také pomocí WAF (web application firewall)?
A nejen před SQL injection, ale i před mnoha dalšími zranitelnostmi vrstvy 7, jako je cross-site scripting, vadná autentizace, cross-site request forgery, odhalení dat a další. Můžete použít vlastní WAF, jako je Mod Security, nebo cloudová řešení, jak je uvedeno níže.
SQL Injection a moderní PHP frameworky
SQL injection je tak běžná, snadná, frustrující a nebezpečná, že všechny moderní webové frameworky PHP mají vestavěné mechanismy pro prevenci. Například ve WordPressu máme funkci $wpdb->prepare()
, zatímco pokud používáte MVC framework, ten za vás odvede veškerou „špinavou práci“ a nemusíte se o prevenci SQL injection vůbec starat. Ve WordPressu musíte připravené příkazy vytvářet explicitně, ale to je prostě WordPress.
Chci tím říci, že moderní weboví vývojáři nemusí o SQL injection vůbec přemýšlet a v důsledku toho si ji ani neuvědomují. I když nechají ve své aplikaci otevřená malá zadní vrátka (např. parametr v $_GET
dotazu a neopatné vložení dotazu), následky mohou být katastrofální. Vždy je tedy lepší najít si čas a proniknout do hloubky základních principů.
Závěr
SQL injection je velmi zákeřný útok na webovou aplikaci, ale lze se mu snadno vyhnout. Jak jsme si v tomto článku ukázali, stačí být opatrný při zpracování uživatelského vstupu (a SQL injection není jedinou hrozbou, kterou manipulace s uživatelským vstupem přináší) a při práci s databázovými dotazy. Ne vždy pracujeme v bezpečném webovém frameworku, proto je vždy lepší být si vědom tohoto typu útoku a nepadnout mu za oběť.