10.6 Security validáció

← 10. DevSec – Security alapok

10.6 Input validáció security szemszögből

Áttekintő

Az előző fejezetekben (8.5) már szó volt arról, hogy a felhasználói inputot validálni kell – akkor főleg a production-ready kód szemszögéből. Most ugyanezt a témát vesszük elő, de security fókusszal.

A különbség nem apró: egy UX-szempontú validáció azt mondja, „a mezőbe szám kerüljön". Egy security-szempontú validáció azt mondja, „semmit ne higgyek el, amit a felhasználó küld – még akkor sem, ha szám érkezik."

Ez az alfejezet arról szól, hogyan gondolkodik egy security-tudatos fejlesztő az inputról. Nem kell security expertté válnod. De ezt az egyetlen szabályt jegyezd meg, és tarts ki mellette:

Never trust user input. Soha, semmilyen körülmények között.


Részletes leírás

A validáció mint biztonsági eszköz – nem csak UX

Sokan azt hiszik, hogy az input validáció azért van, hogy a felhasználó ne hibázzon. Ez igaz – de csak a fele az igazságnak.

A másik fele: az input validáció egy biztonsági kapu. Minden adat, ami a rendszeredbe kerül a külvilágból, potenciális támadási felület.

A külvilág nem csak az ügyfeleidből áll. Egy támadó:

  • közvetlenül HTTP kéréseket küld az API-odra (böngészőt teljesen megkerülve),
  • módosítja a kéréseket (pl. Burp Suite, curl, Postman),
  • automatizált scriptet futtat ezer különböző payloaddal,
  • megkerüli a kliensoldali validációt – az mindig megkerülhető.

A kliensoldali validáció kényelmi funkció. A szerveroldali validáció biztonsági eszköz.

Ha csak kliensen validálsz, az olyan, mintha a bankfiókban a recepciós asztalra kitennél egy táblát: „Lopni tilos." A tolvaj megkerüli a táblát, és bemegy.


Whitelist vs. blacklist megközelítés

Ez az egyik legfontosabb gondolkodásmódbeli döntés validáció esetén.

Blacklist (tiltólista): meghatározod, mi NEM mehet át.

# Blacklist megközelítés – gyenge
def validate_username(username):
    forbidden = ["<", ">", "'", '"', ";", "--"]
    for char in forbidden:
        if char in username:
            return False
    return True

Miért gyenge? Mert a támadó csak olyat keres, amit elfelejtettél tiltani. A blacklist soha nem teljes. Ha SQL injection ellen védesel így, a támadó találni fog egy encodingot vagy egy variánst, ami átcsúszik.

Whitelist (engedélyezési lista): meghatározod, mi mehet át. Minden más tiltott.

# Whitelist megközelítés – erős
import re

def validate_username(username):
    # Csak betű, szám, kötőjel, 3-30 karakter
    pattern = r'^[a-zA-Z0-9\-]{3,30}$'
    return bool(re.match(pattern, username))

Ez a logika: „Pontosan tudom, mi az érvényes. Minden más le van tiltva."

Alapszabály: mindig whitelistelj, ha lehetséges.

Kivétel: szabad szöveges mezőknél (pl. komment, leírás) nem lehet teljesen whitelist alapon menni. Ott a sanitizáció a megoldás (escape, HTML encoding), nem a teljes tiltás.


A szerveroldali validáció elmaradhatatlan

Nézzük meg, mi történik, ha csak kliensoldali validáció van:

// Frontend form validáció – JavaScript
document.getElementById('age').addEventListener('input', function() {
    if (this.value < 0 || this.value > 150) {
        this.setCustomValidity('Érvénytelen kor');
    }
});

Ez a validáció a böngészőben fut. Bárki, aki ismeri a DevTools-t, letilthatja ezt a kódot. Bárki, aki curl-t vagy Postmant használ, soha nem is látja.

Egy támadó így küld kérést:

curl -X POST https://yourapp.com/api/user \
  -H "Content-Type: application/json" \
  -d '{"age": -99999}'

A böngésző validációja ezzel szemben tehetetlen.

Minden inputot a szerveren kell validálni. A kliensoldali validáció csak UX – felhasználói élményt javít, nem biztonságot.


File upload biztonság

A fájlfeltöltés az egyik leggyakrabban elrontott funkció junior fejlesztőknél. Látszólag egyszerű – a felhasználó feltölt egy fájlt, te elmented. Mi ennél az egyszerűbbnél is bonyolódhat?

Sok minden.

1. probléma: fájltípus ellenőrzés csak kiterjesztés alapján

// ROSSZ – csak a nevet nézi, könnyű megkerülni
$extension = pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION);
if ($extension !== 'jpg') {
    die('Csak JPG engedélyezett!');
}

Egy támadó feltölthet egy malware.php-t, amelyet átnevez malware.jpg-re. A kiterjesztés-ellenőrzés megkerülhető.

2. probléma: a MIME type is hamisítható

A $_FILES['file']['type'] értékét a böngésző küldi – tehát a kliens. Hamisítható.

Mit csinálj helyette?

// JOBB – fájl tartalmát ellenőrzöd, nem csak nevét
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($_FILES['file']['tmp_name']);

$allowed = ['image/jpeg', 'image/png', 'image/gif'];

if (!in_array($mimeType, $allowed)) {
    die('Nem engedélyezett fájltípus');
}

Ez már a tényleges fájl tartalmat elemzi, nem a nevet vagy a böngésző által küldött MIME-típust.

3. probléma: a feltöltött fájlt a webszerver futtathatja

Ha a feltöltött fájlok egy olyan mappában landolnak, ahol a webszerver .php (vagy .js, .py) fájlokat futtat, akkor egy támadó PHP kódot tölt fel és közvetlenül futtatja azt.

# Veszélyes konfiguráció – a /uploads mappa PHP-t futtat
yourapp.com/uploads/malware.php → a szerver lefuttatja!

Megoldás:

  • A feltöltési mappa ne legyen közvetlenül web-elérhető, vagy
  • Webszerver konfiguráción tiltsd le a szkriptek futtatását az adott mappában, és
  • A fájlt át kell nevezni (ne az eredeti nevet használd!), random generált névvel mentsd el.
import uuid
import os

def save_upload(file, upload_dir):
    # Ne az eredeti nevet használd!
    safe_filename = str(uuid.uuid4()) + '.jpg'
    save_path = os.path.join(upload_dir, safe_filename)
    file.save(save_path)
    return safe_filename

4. probléma: fájlméret korlát hiánya

Ha nincs méretkorlát, egy támadó feltölt egy 10 GB-os fájlt és feltölti a szerver tárhelyét vagy memóriáját.

MAX_FILE_SIZE = 5 * 1024 * 1024  # 5 MB

if request.content_length > MAX_FILE_SIZE:
    return "Fájl túl nagy", 413

„Never trust user input" a gyakorlatban

Ez nem csak szlogen. Nézzük meg konkrétan, honnan érkezhet „felhasználói input":

ForrásMiért veszélyes
HTML form mezőkMódosíthatók DevToolssal, curl-lel
URL paraméterek?id=1 OR 1=1 – SQL injection próbálkozás
HTTP headerekX-Forwarded-For, User-Agent – hamisíthatók
Cookie-kMódosíthatók, nem megbízhatók
Feltöltött fájlokTartalom és típus is hamisítható
WebhookokKülső rendszer által küldött adat
API válaszokHarmadik fél adatai is validálást igényelnek

Az utolsó sort sokan elfelejtik: ha egy külső API-tól kapsz adatot, azt is validálni kell, mert a külső rendszer kompromittálódhat, vagy csak hibás adatot küldhet.

Praktikus ellenőrzőlista minden inputhoz:

  1. Típus: Szám-e tényleg, ha számot várok? String-e, ha stringet?
  2. Hossz: Van-e minimum és maximum korlát?
  3. Formátum: Megfelel-e a várt mintának (regex, email formátum, stb.)?
  4. Értékkészlet: Felsorolható értékek esetén az engedélyezett listán van?
  5. Szerver oldal: Ez a validáció a szerveren fut, nem csak a kliensen?

Path traversal – egy kevéssé ismert veszély

Ha fájlneveket kezelnek felhasználói inputból, érdemes ismerni ezt a támadást:

# VESZÉLYES
filename = request.args.get('file')
with open(f'/var/app/uploads/{filename}', 'r') as f:
    return f.read()

Egy támadó ezt küldi: file=../../etc/passwd

A szerver így nyitja meg: /var/app/uploads/../../etc/passwd → ami valójában /etc/passwd – a Linux jelszófájl.

Megoldás:

import os

BASE_DIR = '/var/app/uploads'

def safe_open(filename):
    # A valódi teljes útvonalat számítod ki
    requested_path = os.path.realpath(os.path.join(BASE_DIR, filename))
    
    # Ellenőrzöd, hogy a kért fájl tényleg a BASE_DIR-en belül van
    if not requested_path.startswith(BASE_DIR):
        raise ValueError("Hozzáférés megtagadva")
    
    return open(requested_path, 'r')

Összefoglalás – a security validáció lényege

ElvMit jelent a gyakorlatban
Never trust user inputMindent validálj szerveren, függetlenül a klienstől
Whitelist > blacklistDefiniáld, mi érvényes – ne a tiltottat sorold fel
File upload = külön figyelmet igényelMIME, méret, név, futtathatóság
Kliensoldali validáció = UXSoha ne legyen az egyetlen védelmi vonal
Minden forrás gyanúsForm, URL, cookie, header, webhook – mind validálandó

Életszerű példák

1. példa: A „biztonságos" regisztrációs form

Képzeld el ezt a szituációt: kész a regisztrációs form. Frontenden minden validálva van – email formátum, jelszó hossz, felhasználónév karakterkészlet. Teszteled böngészőből, minden működik.

Egy héttel later a support jelenti: valaki a felhasználónévbe <script>alert('XSS')</script>-et írt be, és most minden más felhasználónak megjelenik egy alert, amikor a nevét valahol megjelenítik.

Hogy történhetett? A támadó egyszerűen a DevToolsban letiltotta a JavaScript validációt, és így elküldte a kérést. A backenden nem volt validáció.

Tanulság: A kliensoldali validáció nulledik lépés. A backend validáció az igazi kapu.


2. példa: A profilkép feltöltés

Egy junior fejlesztő implementálja a profilkép feltöltést:

// Amit írt
if ($_FILES['avatar']['type'] == 'image/jpeg') {
    move_uploaded_file($_FILES['avatar']['tmp_name'], 
        'uploads/' . $_FILES['avatar']['name']);
}

Két hiba is van:

  1. A $_FILES['avatar']['type'] a böngésző által küldött érték – hamisítható.
  2. Az eredeti fájlnevet használja – ha valaki shell.php-t tölt fel és image/jpeg MIME-et küld, a szerver elmenti a shell.php-t az uploads/ mappába.

Ha az uploads/ mappa web-elérhető, a támadó megnyitja a böngészőben: yoursite.com/uploads/shell.php – és már tetszőleges PHP kódot futtathat a szerveren.

Megoldás: Valódi MIME-ellenőrzés a fájl tartalma alapján, random fájlnév generálás, az uploads mappa web-elérhetőségének letiltása.


3. példa: Az életkor mező

Egy egészségügyi alkalmazásban van egy életkor mező. Kliensoldali validáció: 0 és 150 között. Logikusnak tűnik.

Egy tesztelő (vagy tesztelés nélkül egy valódi felhasználó) curl-lel beküld -1-et. Az alkalmazás ezt tárolja az adatbázisban. Egy héttel later egy riport kigenerálja az átlagos életkort – és az eredmény értelmetlen, mert (-1 + 35 + 42 + ...) / n nem ugyanolyan, mint kellene.

Nem minden támadás rosszindulatú. Néha csak bugtesztelés, néha véletlenszerű rossz adat. A szerver oldali validáció adatintegritást is véd, nem csak biztonságot.


Tesztfeladatok

1. feladat – Fogalmi megértés

Miért nem elegendő kizárólag kliensoldali (JavaScript) validáció a biztonsághoz? Válassz minden helyes választ!

  • [ ] A. Mert a JavaScript lassabb, mint a szerver oldali kód
  • [x] B. Mert a támadó megkerülheti a böngészőt és közvetlenül küldhet HTTP kérést
  • [x] C. Mert a kliensoldali kód DevToolsban letiltható
  • [ ] D. Mert a JavaScript nem tud regex-et kezelni

2. feladat – Whitelist vs. blacklist

Melyik megközelítés a biztonságosabb egy felhasználónév validálásánál?

A megközelítés:

def validate(name):
    blocked = ["<", ">", "'", "\""]
    return not any(c in name for c in blocked)

B megközelítés:

import re
def validate(name):
    return bool(re.match(r'^[a-zA-Z0-9_]{3,20}$', name))
  • [ ] A. Az A, mert explicit tiltja a veszélyes karaktereket
  • [x] B. A B, mert whitelist alapon csak az engedélyezett karaktereket fogadja el
  • [ ] C. Mindkettő egyformán biztonságos
  • [ ] D. Az A, mert hosszabb és részletesebb

Magyarázat: A blacklist soha nem teljes. A B megközelítés pontosan definiálja, mi érvényes – minden más le van tiltva.


3. feladat – File upload

Egy fejlesztő így ellenőrzi a feltöltött fájl típusát:

if ($_FILES['file']['type'] === 'image/png') {
    // elfogadja
}

Mi a probléma ezzel?

  • [ ] A. A PHP nem tudja kezelni a PNG fájlokat
  • [x] B. A $_FILES['file']['type'] a böngésző által küldött érték, hamisítható
  • [ ] C. Az === operátor nem megfelelő típusellenőrzésre
  • [ ] D. Nincs probléma, ez helyes megközelítés

4. feladat – Szerver oldali validáció

Egy webshopban a termék ára kliensoldali JavaScriptből kerül be a POST kérésbe. A szerver ezt közvetlenül feldolgozza:

price = float(request.form['price'])
total = price * quantity
process_payment(total)

Mi a biztonsági probléma?

  • [ ] A. A Python float() függvény nem elég pontos
  • [ ] B. A szorzás nem helyes
  • [x] C. A felhasználó a POST kérésben tetszőleges árat küldhet (pl. 0.01 Ft-ot), amit a szerver elfogad
  • [ ] D. Nincs probléma, a kliensoldali validáció elegendő

5. feladat – Path traversal

Egy fájl letöltő endpoint így működik:

filename = request.args.get('file')
with open(f'/var/uploads/{filename}') as f:
    return f.read()

Mi a veszély, ha egy támadó file=../../etc/passwd-t küld?

  • [ ] A. A Python hibát dob, mert a .. nem érvényes fájlnév
  • [x] B. A szerver megnyitja a /etc/passwd fájlt és visszaküldi a tartalmát
  • [ ] C. A request.args.get automatikusan sanitizálja az inputot
  • [ ] D. Nincs veszély, a fájlrendszer megvédi magát

6. feladat – File upload mentési stratégia

Melyik a helyes megközelítés feltöltött fájlok mentésénél?

  • [ ] A. Az eredeti fájlnevet menteni, mert a felhasználónak szüksége van rá
  • [ ] B. A MIME típust a $_FILES['type'] alapján ellenőrizni
  • [x] C. Random generált fájlnevet használni, és a fájl tartalmából meghatározni a MIME típust
  • [ ] D. Csak a fájlkiterjesztést ellenőrizni

7. feladat – Forrás azonosítás

Melyik adatforrások igényelnek szerver oldali validációt? (Több jó válasz is lehet!)

  • [x] A. HTML form adatok
  • [x] B. URL query paraméterek
  • [x] C. Cookie értékek
  • [x] D. Harmadik fél API-tól érkező webhook adat
  • [ ] E. Az adatbázisban tárolt adatok, amelyeket már korábban validáltunk

8. feladat – Életszerű szituáció

Egy junior fejlesztő implementál egy komment funkciót. A felhasználó szabad szöveget írhat (whitelist nem alkalmazható). Mi a helyes megközelítés?

  • [ ] A. Blacklist: tiltani a <script> tageket
  • [x] B. HTML encoding / escape a megjelenítésnél, hogy a szöveg ne kerüljön értelmezésre
  • [ ] C. Nem kell semmit csinálni, a böngésző kezeli
  • [ ] D. A kommentek teljes tiltása, ha nem lehet whitelistelni

9. feladat – Méretkorlát

Miért fontos a feltöltött fájlok méretének szerver oldali korlátozása?

  • [ ] A. Mert a nagy fájlok lassítják a böngészőt
  • [x] B. Mert korlátlan méret esetén egy támadó feltölti a szerver tárhelyét vagy memóriáját (DoS)
  • [ ] C. Mert a fájlrendszer nem tud 1 GB-nál nagyobb fájlt kezelni
  • [ ] D. Nincs különösebb biztonsági jelentősége

10. feladat – Összefoglalás

Egy kezdő fejlesztő azt mondja: „Nálunk frontend validáció van, és a felhasználóink megbízhatók, szóval jó vagyunk." Mi a helyes válasz?

  • [ ] A. Ha a felhasználók megbízhatók, valóban elegendő a kliensoldali validáció
  • [ ] B. Csak érzékeny funkcióknál kell szerveroldali validáció
  • [x] C. A szerveroldali validáció nem a felhasználók megbízhatóságáról szól – egy támadó nem használja a frontendet
  • [ ] D. A kliensoldali és a szerveroldali validáció ugyanazt a védelmet nyújtja
Scroll to Top