SQL injection – jak funguje a jak se jí spolehlivě bránit
SQL injection na konkrétních příkladech v PHP, Pythonu a Node.js. Parametrizované dotazy, ORM, pasti dynamického ORDER BY a jak útok otestovat.
SQL injection je jedna z nejstarších zranitelností webu, a přesto se stále objevuje. Princip je jednoduchý: aplikace skládá SQL dotaz z textu, který zadal uživatel, a ten text změní význam dotazu. Dobrá zpráva je, že obrana je stejně jednoduchá – stačí ji důsledně dodržovat.
Jak útok vypadá
Typický zranitelný kód v PHP:
$email = $_POST['email'];
$sql = "SELECT * FROM users WHERE email = '$email'";
$result = $pdo->query($sql);
Útočník do pole zadá:
' OR '1'='1
Z dotazu se stane:
SELECT * FROM users WHERE email = '' OR '1'='1'
Podmínka platí pro všechny řádky. U přihlašovacího formuláře to může znamenat přihlášení bez hesla, u vyhledávání výpis celé tabulky. Pokročilejší varianty (UNION-based, blind, time-based) umožňují postupně vytáhnout libovolná data z databáze.
Jediná správná obrana: parametrizované dotazy
Parametrizovaný dotaz (prepared statement) posílá databázi zvlášť strukturu dotazu a zvlášť hodnoty. Hodnota se nikdy neinterpretuje jako SQL, ať obsahuje cokoli.
PHP (PDO)
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email');
$stmt->execute(['email' => $_POST['email']]);
$user = $stmt->fetch();
U PDO s MySQL je dobré nastavit PDO::ATTR_EMULATE_PREPARES => false, aby se používaly skutečné prepared statements na straně serveru.
Python (psycopg / sqlite3)
# psycopg (PostgreSQL) používá %s
cur.execute("SELECT * FROM users WHERE email = %s", (email,))
# sqlite3 používá ?
cur.execute("SELECT * FROM users WHERE email = ?", (email,))
Pozor: cur.execute("... WHERE email = '%s'" % email) nebo f-string není parametrizace, je to obyčejné skládání řetězců.
Node.js (mysql2, pg)
// mysql2
const [rows] = await conn.execute('SELECT * FROM users WHERE email = ?', [email]);
// pg
const res = await client.query('SELECT * FROM users WHERE email = $1', [email]);
ORM vás chrání – dokud ho neobejdete
Django ORM, SQLAlchemy, Eloquent, Prisma a další generují parametrizované dotazy automaticky. Problém nastane u „surových“ dotazů:
# Django – ZRANITELNÉ
User.objects.raw(f"SELECT * FROM auth_user WHERE username = '{name}'")
# Django – správně
User.objects.raw("SELECT * FROM auth_user WHERE username = %s", [name])
V Prismě je rozdíl mezi $queryRaw (tagged template, parametrizuje) a $queryRawUnsafe (neparametrizuje). Název funkce varuje oprávněně.
Past: identifikátory nejdou parametrizovat
Parametry fungují jen pro hodnoty. Název tabulky, sloupce nebo směr řazení přes ně předat nelze. Častá chyba je řazení podle parametru z URL:
# ZRANITELNÉ
sql = f"SELECT * FROM products ORDER BY {request.args['sort']}"
Řešení je allowlist:
ALLOWED_SORT = {"name": "name", "price": "price", "newest": "created_at DESC"}
order = ALLOWED_SORT.get(request.args.get("sort"), "name")
sql = f"SELECT * FROM products ORDER BY {order}"
Uživatel volí klíč, do SQL jde jen hodnota, kterou jste napsali vy.
Podobně u LIKE a IN
- LIKE: parametr předejte i se zástupnými znaky:
WHERE name LIKE %sa hodnotaf"%{hledany}%". Pokud nechcete, aby uživatel mohl sám zadávat%a_, escapujte je. - IN: vygenerujte správný počet zástupných znaků, ne hodnoty:
ids = [3, 7, 9]
placeholders = ",".join(["%s"] * len(ids))
cur.execute(f"SELECT * FROM items WHERE id IN ({placeholders})", ids)
Druhá linie obrany
Parametrizace je základ, ale škody se dají dál omezit:
- Minimální oprávnění DB účtu. Webová aplikace nepotřebuje
DROP,GRANTani přístup k jiným databázím. - Nevypisujte chyby databáze uživateli. Detailní chybová hláška útočníkovi prozradí strukturu dotazu.
- Validace vstupu (např. že ID je číslo) – není náhradou parametrizace, ale snižuje plochu útoku.
- WAF může zachytit známé vzory, ale nespoléhejte na něj jako na jedinou ochranu.
Jak si aplikaci otestovat
Na vlastním (nikdy cizím!) prostředí můžete použít nástroj sqlmap:
sqlmap -u "http://localhost:8000/search?q=test" --batch --level=2
Rychlá ruční kontrola: do vstupů zadejte apostrof '. Pokud se objeví chyba SQL nebo HTTP 500, máte podezřelé místo. Pro statickou analýzu kódu pomůže Semgrep nebo pro Python Bandit (bandit -r .), který upozorní na skládání SQL řetězců.
Shrnutí
- Každá hodnota od uživatele jde do dotazu jako parametr.
- Identifikátory (tabulky, sloupce, řazení) jen přes allowlist.
- Pozor na „raw“ funkce v ORM.
- DB účet s minimem práv a skryté chybové hlášky.
Dodržíte-li tyto čtyři body, SQL injection prakticky přestane být vaším problémem.