10.3 SQL injection

← 10. DevSec – Security alapok

10.3 SQL injection megelőzése – paraméteres lekérdezések

Áttekintő

Az SQL injection az egyik legregebbi és legveszélyesebb sérülékenység a webfejlesztésben. Az OWASP Top 10 listán évtizedek óta szerepel – és nem azért, mert nehéz kivédeni, hanem azért, mert meglepően sokan elfelejtik.

A jó hír: ha egyszer megérted, miért működik az SQL injection, a védelem triviálisan egyszerű. Nem kell security expert lenned ahhoz, hogy soha ne kövesd el ezt a hibát.

Ez az alfejezet megmutatja:

  • hogyan működik az SQL injection mechanizmusa
  • miért nem elég a manuális escapelés
  • hogyan használsz paraméteres lekérdezéseket / prepared statement-eket
  • mikor véd az ORM és mikor nem
  • mire figyelj raw query írásakor

Részletes leírás

Hogyan működik az SQL injection?

Képzeld el a következő kódot:

# Python – SEBEZHETŐ KÓD, NE CSINÁLD
username = request.form['username']
password = request.form['password']

query = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'"
db.execute(query)

Ha a felhasználó normálisan tölti ki az űrlapot (alice / titkos123), a lekérdezés így néz ki:

SELECT * FROM users WHERE username = 'alice' AND password = 'titkos123'

Eddig semmi gond. Most képzeld el, hogy valaki a username mezőbe ezt írja:

' OR '1'='1

Az összerakott lekérdezés:

SELECT * FROM users WHERE username = '' OR '1'='1' AND password = 'bármi'

Az '1'='1' mindig igaz. A feltétel teljesül minden sorra. A támadó belépett az első felhasználó (gyakran admin) fiókjába – jelszó nélkül.

Egy még veszélyesebb példa, ha valaki ezt írja:

'; DROP TABLE users; --

Eredmény:

SELECT * FROM users WHERE username = ''; DROP TABLE users; --' AND password = '...'

A -- kommentbe teszi a maradék SQL-t. Az összes felhasználói adat törlődik.


Miért nem elég a manuális escapelés?

Az a gondolat, hogy „majd kiszűröm az aposztrófot" – csapdába visz.

1. probléma: elfelejtesz valamit

Az SQL injection megelőzéséhez minden egyes dinamikus értéket helyesen kell kezelni. Egyetlen kihagyott mező elég. Az emberek felejtnek, a prepared statement nem.

2. probléma: charset-trükkök

Egyes karakterkódolásokban (pl. régi GBK charset MySQL-ben) megkerülhető az escape-elés. Az adatbázis driver ezeket már kezeli helyesen.

3. probléma: nehéz jól csinálni

Az egyes adatbázismotorok escapelési szabályai eltérnek. Amit MySQL-re jól írtál, PostgreSQL-re esetleg rosszul viselkedik.

Következtetés: Ne próbálj manuálisan escapelni. Használj paraméteres lekérdezést – erre találták ki.


Paraméteres lekérdezések / Prepared Statement-ek

A prepared statement alapelve: az SQL struktúrát és az adatot külön küldöd az adatbázisnak.

Az adatbázis motor először értelmezi az SQL struktúrát (a ? helyőrzőkkel együtt), majd az adatokat illeszti be – de soha nem értelmezi az adatot SQL kódként.

# Python (psycopg2 – PostgreSQL) – BIZTONSÁGOS
username = request.form['username']
password = request.form['password']

query = "SELECT * FROM users WHERE username = %s AND password = %s"
cursor.execute(query, (username, password))

Ha a felhasználó beírja a ' OR '1'='1 trükköt, az adatbázis ezt literális szövegként kezeli. Nem fog egyetlen felhasználónévvel sem egyezni – a támadás nem működik.

// JavaScript (node-postgres) – BIZTONSÁGOS
const query = {
  text: 'SELECT * FROM users WHERE username = $1 AND password = $2',
  values: [username, password],
};
await client.query(query);
// PHP (PDO) – BIZTONSÁGOS
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ? AND password = ?");
$stmt->execute([$username, $password]);

A ?, %s, $1 szintaxis adatbázismotoronként és drivertől függően változik – de az elv mindenhol ugyanaz: ne fűzz össze stringet SQL-lel.


ORM-ek és az SQL injection

A legtöbb modern ORM (SQLAlchemy, Eloquent, Django ORM, Prisma, TypeORM) alapból paraméteres lekérdezéseket használ a lekérdezőjénél. Ha a szokásos ORM API-t használod, jó eséllyel védett vagy.

# Django ORM – BIZTONSÁGOS
# A Django automatikusan paraméteres lekérdezést használ
user = User.objects.filter(username=username, password=hashed_password).first()
// Laravel Eloquent – BIZTONSÁGOS
$user = User::where('username', $username)->where('password', $hashedPassword)->first();

DE: az ORM nem nyújt védelmet raw query esetén.

# Django – SEBEZHETŐ, ha raw query-t használsz rosszul
User.objects.raw("SELECT * FROM users WHERE username = '" + username + "'")  # VESZÉLYES
# Django – BIZTONSÁGOS raw query
User.objects.raw("SELECT * FROM users WHERE username = %s", [username])  # OK

Szabály: Ha raw query-t kell írnod (pl. komplex JOIN, adatbázis-specifikus függvény), mindig paraméteres formát használj.


Raw query – mikor indokolt és hogyan csináld biztonságosan

Néha az ORM nem elég rugalmas: komplex aggregáció, rekurzív lekérdezés, adatbázis-specifikus funkció, teljesítményoptimalizálás.

Ilyenkor:

Mindig használj paramétert az értékekhez

# SQLAlchemy – raw query, biztonságosan
result = db.execute(
    text("SELECT * FROM orders WHERE user_id = :user_id AND status = :status"),
    {"user_id": user_id, "status": status}
)

Dinamikus tábla- vagy oszlopnevet NE adj meg paraméterként – ezt az adatbázis nem engedi, és nem is érdemes, mert ilyenkor whitelist-et kell alkalmaznod:

# Whitelist-alapú megközelítés dinamikus oszlop esetén
ALLOWED_SORT_COLUMNS = {'name', 'created_at', 'price'}

sort_column = request.args.get('sort', 'name')
if sort_column not in ALLOWED_SORT_COLUMNS:
    sort_column = 'name'  # fallback biztonságos értékre

query = f"SELECT * FROM products ORDER BY {sort_column}"  # most már biztonságos

Gyors összefoglalás: mit csinálj, mit ne

SzituációTeendő
ORM lekérdezőt használszAlapból védett – ne aggódj
String concatenation SQL-beSOHA
Raw query értékkelParaméteres (?, %s, :name)
Raw query tábla/oszlop névvelWhitelist, nem paraméter
Manuális escapelésNe – használj prepared statement-et helyette

Életszerű példák

1. példa: bejelentkezési form

Sebezhető verzió:

// Node.js + MySQL – NE CSINÁLD
const query = `SELECT * FROM users WHERE email = '${email}' AND password = '${password}'`;
connection.query(query, callback);

Biztonságos verzió:

// Node.js + MySQL – BIZTONSÁGOS
const query = "SELECT * FROM users WHERE email = ? AND password = ?";
connection.query(query, [email, password], callback);

2. példa: keresés szabad szöveggel

Keresőmező értéke kerül a lekérdezésbe – klasszikus injection pont.

// PHP – SEBEZHETŐ
$search = $_GET['q'];
$result = $pdo->query("SELECT * FROM products WHERE name LIKE '%" . $search . "%'");
// PHP – BIZTONSÁGOS
$search = $_GET['q'];
$stmt = $pdo->prepare("SELECT * FROM products WHERE name LIKE ?");
$stmt->execute(['%' . $search . '%']);

Figyelj: a % karaktert a PHP oldalon fűzöd a paraméterhez, nem az SQL stringbe.


3. példa: rendező oszlop URL paraméterből

Ez egy olyan eset, ahol a paraméteres lekérdezés nem működik (oszlopnevet nem lehet paraméterezni), de whitelist igen.

# SEBEZHETŐ
sort = request.args.get('sort')
db.execute(f"SELECT * FROM articles ORDER BY {sort}")

# BIZTONSÁGOS
ALLOWED = {'title', 'published_at', 'views'}
sort = request.args.get('sort', 'published_at')
if sort not in ALLOWED:
    sort = 'published_at'
db.execute(f"SELECT * FROM articles ORDER BY {sort}")

4. példa: ORM és raw query keveredése

# Django – ORM részek biztonságosak, a raw belső rész nem
from django.db import connection

def get_user_stats(user_id):
    # Ez biztonságos – ORM paraméteres
    user = User.objects.get(id=user_id)

    # Ez VESZÉLYES – raw query string concatenation
    with connection.cursor() as cursor:
        cursor.execute(f"SELECT COUNT(*) FROM orders WHERE user_id = {user_id}")  # BAD

    # Ez BIZTONSÁGOS
    with connection.cursor() as cursor:
        cursor.execute("SELECT COUNT(*) FROM orders WHERE user_id = %s", [user_id])  # GOOD

Tesztfeladatok

1. kérdés

Az alábbi kódrészlet sebezhető SQL injection ellen?

username = input()
query = "SELECT * FROM users WHERE username = '" + username + "'"
cursor.execute(query)

A) Igen, mert string összefűzést használ felhasználói inputtal
B) Nem, mert Python biztonságos a stringek kezelésében
C) Igen, de csak akkor, ha az input aposztrofot tartalmaz
D) Nem, mert a SELECT nem módosítja az adatbázist


2. kérdés

Melyik az SQL injection ellen biztonságos megoldás?

A)

$query = "SELECT * FROM users WHERE id = " . intval($id);

B)

$query = "SELECT * FROM users WHERE id = ?";
$stmt = $pdo->prepare($query);
$stmt->execute([$id]);

C)

$id = str_replace("'", "''", $id);
$query = "SELECT * FROM users WHERE name = '$id'";

D) Mindkét A) és B) egyformán biztonságos


3. kérdés

Egy Django ORM lekérdezés:

User.objects.filter(username=request.POST['username'])

Sebezhető SQL injection ellen?

A) Igen, mert a request.POST-ból olvas
B) Nem, mert a Django ORM automatikusan paraméteres lekérdezést használ
C) Csak akkor biztonságos, ha a username mezőre van egyedi index
D) Attól függ, milyen adatbázist használ


4. kérdés

Mit jelent a „prepared statement" lényege?

A) Az SQL lekérdezés előre le van ellenőrizve, hogy helyes szintaxisú
B) Az SQL struktúra és az adatok külön kerülnek az adatbázishoz, az adat nem értelmezhető SQL kódként
C) Az adatbázis gyorsítótárazza a lekérdezést a teljesítmény érdekében
D) A felhasználói input automatikusan titkosítva lesz


5. kérdés

Az alábbi kód melyik részén van SQL injection sérülékenység?

def get_products(category, sort_column):
    allowed_sorts = {'name', 'price', 'created_at'}
    if sort_column not in allowed_sorts:
        sort_column = 'name'
    
    query = text("SELECT * FROM products WHERE category = :cat ORDER BY " + sort_column)
    return db.execute(query, {"cat": category})

A) A category paramétert nem biztonságosan kezeli
B) A sort_column whitelist ellenőrzés hiányos
C) Nincs SQL injection sérülékenység, mindkét érték biztonságosan van kezelve
D) A text() függvény önmagában nem elegendő a védelemhez


6. kérdés

Miért nem elég az apostrofot kiszűrni a felhasználói inputból a SQL injection ellen?

A) Mert vannak más speciális karakterek is (pl. ", \, ;)
B) Mert az escapelési szabályok adatbázisonként és karakterkódolásonként eltérhetnek, és komplex esetekben megkerülhetők
C) Mert a jelszavakban jogosan szerepelhet aposztof
D) Mindhárom fenti válasz igaz


7. kérdés

Melyik az egyetlen helyzet, ahol paraméteres lekérdezéssel nem lehet megoldani az SQL injection védelmet?

A) Ha a WHERE feltételben dinamikus érték szerepel
B) Ha a táblanevet vagy oszlopnevet dinamikusan kell meghatározni
C) Ha LIKE operátort használsz
D) Ha a lekérdezésben JOIN szerepel


8. kérdés

Egy junior fejlesztő így érvel: „Mi ORM-et használunk, tehát nem kell SQL injection-nel foglalkoznunk."

Mikor HELYTELEN ez a kijelentés?

A) Soha – az ORM mindig véd
B) Ha az ORM raw query metódusát string összefűzéssel használják
C) Ha a lekérdezés összetett (több JOIN-nal)
D) Ha az adatbázis MySQL és nem PostgreSQL


9. kérdés

Az alábbi Laravel Eloquent kód biztonságos SQL injection ellen?

$users = DB::select("SELECT * FROM users WHERE role = '" . $role . "'");

A) Igen, mert Laravel automatikusan escapeli az inputokat
B) Nem, mert a DB::select nem véd string összefűzés esetén
C) Igen, mert a role mező általában nem tartalmaz speciális karaktert
D) Attól függ, hogy a $role értéke honnan érkezik


10. kérdés

Melyik állítás igaz a paraméteres lekérdezésekről?

A) Lassabb végrehajtást eredményez, ezért éles rendszerben nem érdemes használni
B) Csak SQL injection ellen véd, más sérülékenységek ellen nem
C) Opcionális optimalizálás – a jó escapelés ugyanolyan biztonságos
D) Minden modern webalkalmazásban kötelező alapkövetelmény, teljesítménybüntetés nélkül


Helyes válaszok

  1. A – String összefűzés felhasználói inputtal mindig sebezhető
  2. B – A ? placeholder az egyetlen teljesen megbízható megoldás; az A) intval() véd integer esetén, de más típusoknál nem általánosítható
  3. B – A Django ORM filter metódusai automatikusan paraméteres lekérdezést generálnak
  4. B – Ez a prepared statement lényege: struktúra és adat szétválasztása
  5. C – A kód helyes: a category paraméteres (:cat), a sort_column whitelist-elve van
  6. D – Mindhárom ok valós probléma; a komplex escapelés megkerülhető
  7. B – Tábla- és oszlopnevekre nem alkalmazható paraméteres lekérdezés, csak whitelist
  8. B – Az ORM raw query metódusa (DB::raw, cursor.execute(f"...")) nem véd, ha string összefűzést használsz
  9. B – A DB::select-be írt string concatenation ugyanolyan veszélyes, mint bármely más esetben
  10. B – A paraméteres lekérdezés SQL injection ellen véd; XSS, CSRF ellen külön védelemre van szükség; teljesítménybüntetés nincsen, ez bevett alapgyakorlat
Scroll to Top