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álsz | Alapból védett – ne aggódj |
| String concatenation SQL-be | SOHA |
| Raw query értékkel | Paraméteres (?, %s, :name) |
| Raw query tábla/oszlop névvel | Whitelist, nem paraméter |
| Manuális escapelés | Ne – 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
- A – String összefűzés felhasználói inputtal mindig sebezhető
- 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ó - B – A Django ORM filter metódusai automatikusan paraméteres lekérdezést generálnak
- B – Ez a prepared statement lényege: struktúra és adat szétválasztása
- C – A kód helyes: a
categoryparaméteres (:cat), asort_columnwhitelist-elve van - D – Mindhárom ok valós probléma; a komplex escapelés megkerülhető
- B – Tábla- és oszlopnevekre nem alkalmazható paraméteres lekérdezés, csak whitelist
- B – Az ORM raw query metódusa (
DB::raw,cursor.execute(f"...")) nem véd, ha string összefűzést használsz - B – A
DB::select-be írt string concatenation ugyanolyan veszélyes, mint bármely más esetben - 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