Programování

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.

12. 10. 2026 · 4 min čtení

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 %s a hodnota f"%{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, GRANT ani 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í

  1. Každá hodnota od uživatele jde do dotazu jako parametr.
  2. Identifikátory (tabulky, sloupce, řazení) jen přes allowlist.
  3. Pozor na „raw“ funkce v ORM.
  4. 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.