10.5 Jelszókezelés

← 10. DevSec – Security alapok

10.5 Jelszavak és érzékeny adatok kezelése

Áttekintő

Minden héten hallani arról, hogy valamelyik cég adatbázisát feltörték, és kiszivárgott felhasználók millióinak jelszava. Az esetek nagy részében az derül ki: a cég nem megfelelően tárolta a jelszavakat. Nem arról van szó, hogy hiányzott a tűzfal, vagy valamilyen exotikus exploit élt – egyszerűen csak rosszul csinálták az alapokat.

Ez az alfejezet arról szól, hogy miért nem elég a titkosítás, hogyan kell jelszavakat valóban biztonságosan tárolni, és hogyan kezeld az érzékeny adatokat (API kulcsok, felhasználói adatok, személyes információk) a mindennapi fejlesztésben. Nem kell security expertnek lenned – de ezeket az alapokat egy cég elvárja tőled az első munkanapodtól.

Amit ebben az alfejezetben megtanulsz:

  • Miért plaintext és MD5 jelszótárolás időzített bomba
  • Mi a különbség a titkosítás és a hashing között, és miért számít
  • Hogyan működik a bcrypt/argon2 (anélkül, hogy kriptográfus legyél)
  • Miért kerül a .env fájl a .gitignore-ba, és mi történik, ha nem kerül oda
  • Hogyan kezeld a PII (személyes azonosításra alkalmas) adatokat felelősen

Részletes leírás

A jelszótárolás négy szintje – és melyik hol áll

Képzelj el egy skálát, amelyen a „teljesen felelőtlen" és a „production-ready" két végpont:

VESZÉLYES                                            BIZTONSÁGOS
    |                                                     |
plaintext  →  MD5/SHA1  →  SHA256 (só nélkül)  →  bcrypt/argon2

Nézzük végig, miért nem elég az első három.


1. Plaintext tárolás – az alapvető hiba

# ❌ SOHA NE CSINÁLD EZT
user = {
    "email": "user@example.com",
    "password": "titkos123"  # plaintext a DB-ben
}

Ha valaki megszerzi az adatbázist (SQL injection, backup kiszivárgás, bennfentes hozzáférés), azonnal hozzáfér minden jelszóhoz. És mivel az emberek sok helyen ugyanazt a jelszót használják, ez nemcsak a te rendszered – hanem a felhasználó bankja, e-mailje és minden más fiókja is kompromittálódik.


2. MD5 és SHA1 – már nem elég

Az MD5 és SHA1 gyors hash algoritmusok. Ez éppen a probléma. Egy modern GPU másodpercenként több milliárd MD5 hash-t képes kiszámolni. Ez azt jelenti, hogy a teljes „közönséges jelszó" teret néhány perc alatt le lehet fésülni.

import hashlib

# ❌ SHA1 – ugyanolyan problémás, mint az MD5
password_hash = hashlib.sha1("titkos123".encode()).hexdigest()
# e36e7b3e... – de rainbow table-ből másodpercek alatt visszafejtik

A rainbow table egy előre kiszámolt hash-adatbázis. Feltöltöd a hash-t, visszakapod a jelszót. Vannak publikus rainbow table-ök, amelyek a legtöbb MD5 hash-t azonnal visszafejtik.


3. Miért kell a só (salt)?

A só egy véletlenszerű karakterlánc, amelyet a jelszóhoz fűznek hash előtt:

jelszó: "titkos123"
só: "x7kQ9m"
hash inputja: "titkos123x7kQ9m"

Ezzel két dolgot érünk el:

  • Két felhasználó ugyanolyan jelszava különböző hash-t kap (mert különböző sót kapnak)
  • Rainbow table-ök hatástalanok (mert az adott sóval előre kiszámolt tábla nem létezik)

A só nem titok – tárolhatod ugyanabban az adatbázisban, ahol a hash van. Szerepe nem a titkosság, hanem az egyediesítés.


4. bcrypt és argon2 – a helyes megoldás

A bcrypt és az argon2 szándékosan lassú hash algoritmusok. Ez az ő lényegük.

import bcrypt

# ✅ Jelszó hash-elése bcrypt-tel
password = "titkos123".encode()
salt = bcrypt.gensalt()  # automatikusan generál sót
hashed = bcrypt.hashpw(password, salt)

# ✅ Jelszó ellenőrzése (bejelentkezéskor)
is_correct = bcrypt.checkpw(password, hashed)
const bcrypt = require('bcrypt');

// ✅ JavaScript/Node.js
const saltRounds = 12;  // a „cost factor" – minél nagyobb, annál lassabb
const hashed = await bcrypt.hash('titkos123', saltRounds);

const isCorrect = await bcrypt.compare('titkos123', hashed);
// ✅ PHP – beépített password_hash()
$hashed = password_hash('titkos123', PASSWORD_BCRYPT);

// Ellenőrzés
$isCorrect = password_verify('titkos123', $hashed);

A cost factor (work factor) szabályozza, mennyire lassú a hash. Ha 12-t használsz, a bcrypt kb. 250 ms-t tölt hash-eléssel – egy felhasználónak ez nem feltűnő, de egy támadónak, aki milliószámra próbálkozik, óriási akadály.

Argon2 az újabb ajánlott algoritmus (a bcrypt-nél is erősebb), de bcrypt-tel is production-biztos rendszert lehet építeni.

Ökölszabály: Soha ne írj saját jelszóhash implementációt. Használd a platformod beépített, jól tesztelt könyvtárát.


A .env fájl és a gitignore – miért kritikus

Mi az a .env?

A .env egy fájl, amelybe az alkalmazás konfigurációs értékeit tesszük – különösen azokat, amelyeknek nem szabad a kódban lenniük:

# .env – SOHA NE KERÜLJÖN GIT-BE
DATABASE_URL=postgresql://user:password@localhost/myapp
JWT_SECRET=v3ry_s3cr3t_k3y_here
STRIPE_API_KEY=sk_live_xxxxxxxxxxx
SENDGRID_API_KEY=SG.xxxxxxxxxxxx
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/xxxx

Mi történik, ha Git-be kerül?

Ez az egyik leggyakoribb security incidens junior fejlesztőknél. A folyamat általában így néz ki:

  1. Junior fejlesztő létrehozza a .env fájlt az API kulcsokkal
  2. Lefuttatja a git add . parancsot
  3. Commitolja és pusholja GitHub-ra
  4. A repo public (vagy valaki hozzáfér)
  5. Automatizált botok perceken belül megtalálják és ellopják a kulcsokat
  6. AWS esetén: a bot virtuális gépeket indít, kriptobányászatra, te fizeted a számlát (akár több ezer dollár egy nap alatt)

Ez nem elmélet – ez rendszeresen megtörténik, és AWS/GitHub rendszeresen küldenek figyelmeztetőket kompromittált kulcsok esetén.

# .gitignore – ezt mindig add a projekthez
.env
.env.local
.env.*.local
*.pem
*.key
secrets/

Ha már kikerült – mit tegyél?

Ha véletlenül commitoltad a kulcsot:

  1. Azonnal von vissza (revoke) minden érintett API kulcsot – ez az első lépés, nem a git history tisztítása
  2. Ellenőrizd, volt-e jogosulatlan hozzáférés
  3. Tisztítsd meg a git history-t (git filter-branch vagy BFG Repo Cleaner)
  4. A history tisztítása után is: a régi kulcs már kompromittált, soha ne használd újra

.env.example – a helyes minta

# .env.example – ez KERÜL git-be, értékek nélkül
DATABASE_URL=postgresql://user:password@localhost/myapp
JWT_SECRET=change_me_to_a_random_string
STRIPE_API_KEY=your_stripe_key_here

A .env.example mutatja, milyen változókra van szükség, de nem tartalmaz valódi értékeket. Így új fejlesztő tudja, mit kell beállítania, de a titkok nem kerülnek nyilvánosságra.


HTTPS és adatátvitel biztonsága

Miért nem elég a HTTPS az adatbázisnál?

HTTPS védi az adatot átvitel közben – a felhasználó böngészője és a szervered között. De az adatbázisban tárolt jelszót nem védi. Ezért kell a hashing.

Felhasználó böngészője  →[HTTPS]→  Szerver  →[?]→  Adatbázis
     (titkosítva)                               (itt kell bcrypt!)

Fejlesztési gondolkodásmód HTTPS-sel kapcsolatban

  • Production-ban mindig HTTPS – ma már ingyenes (Let's Encrypt)
  • Jelszavakat soha ne küldj HTTP-n – még teszteléskor sem érdemes megszokni
  • API kulcsokat csak HTTPS-en küldj – header-ben, nem URL paraméterként (az URL-ek logokban jelennek meg)
# ❌ API kulcs URL-ben – megjelenik a logokban
GET /api/data?api_key=sk_live_xxxx

# ✅ API kulcs header-ben
GET /api/data
Authorization: Bearer sk_live_xxxx

PII – Személyes azonosításra alkalmas adatok

A PII (Personally Identifiable Information) olyan adat, amellyel egy személy azonosítható: név, e-mail cím, telefonszám, születési dátum, lakcím, IP cím, stb.

Miért számít a junior fejlesztőnek?

GDPR (EU) és más adatvédelmi törvények alapján a cégek felelősek a PII-ért. Ha te írod a kódot, ami PII-t kezel, te felszel felelős a helyes implementációért.

Alapelvek PII kezelésnél

1. Ne gyűjtsd, ha nem kell

# ❌ Felesleges adat gyűjtése
user_profile = {
    "name": name,
    "email": email,
    "birth_date": birth_date,  # Valóban szükséges?
    "phone": phone,            # Valóban szükséges?
    "ip_address": ip_address   # Valóban szükséges?
}

2. Ne logolj személyes adatot

# ❌ PII a logban
logger.info(f"User login: {user.email} with password {password}")

# ✅ Minimális logolás
logger.info(f"User login attempt: user_id={user.id}")

3. Maszkold, ha megjeleníteni kell

# ✅ Email maszkolása (pl. hibaüzenetben)
def mask_email(email):
    parts = email.split("@")
    return parts[0][:2] + "***@" + parts[1]
# "joska@example.com" → "jo***@example.com"

4. Ne tárold tovább, mint szükséges Ha felhasználó törli fiókját: az adatait is törölni kell (nem csak inactive-re állítani).


Életszerű példák

1. példa: Az „egyszerű" regisztrációs endpoint

Egy junior fejlesztő megkapja az első feladatát: implementálja a regisztrációt.

# ❌ Amit egy junior elsőre megírhat
@app.route('/register', methods=['POST'])
def register():
    email = request.form['email']
    password = request.form['password']
    
    # Direkt tárolja a jelszót
    user = User(email=email, password=password)
    db.session.add(user)
    db.session.commit()
    
    return jsonify({"status": "ok"})
# ✅ Amit production-ban kell csinálni
import bcrypt

@app.route('/register', methods=['POST'])
def register():
    email = request.form['email']
    password = request.form['password'].encode('utf-8')
    
    # Hash-eli a jelszót, mielőtt adatbázisba kerülne
    hashed_password = bcrypt.hashpw(password, bcrypt.gensalt())
    
    user = User(email=email, password_hash=hashed_password)
    db.session.add(user)
    db.session.commit()
    
    return jsonify({"status": "ok"})

A változtatás: a jelszó soha nem kerül az adatbázisba – csak a hash.


2. példa: A véletlen Git push

Péter csatlakozik egy új csapathoz. Klónozza a repót, létrehozza a .env fájlt a fejlesztési kulcsokkal, majd pánikszerűen felfedezi:

git log --oneline
a1b2c3d Add environment configuration  ← ez a commit tartalmazza a .env-t
# Mi történt?
git show a1b2c3d --stat
# .env fájl megjelenik a módosított fájlok között

Helyes reakció:

  1. Azonnal visszavonja (revoke) az összes kulcsot a szolgáltatóknál (AWS console, Stripe dashboard, stb.)
  2. Új kulcsokat generál
  3. Értesíti a csapatvezető/senior fejlesztőt
  4. Utána tisztítja meg a git history-t

Rossz reakció: Csak törli a fájlt és commitolja – a kulcs benne marad a history-ban.


3. példa: Hibaüzenet, ami túl sokat árul el

# ❌ Stack trace a felhasználónak – security anti-pattern
@app.errorhandler(500)
def server_error(e):
    return str(e), 500
# Kimenet: "psycopg2.OperationalError: FATAL: password authentication failed 
#           for user 'myapp_user' at host 'db.internal.company.com:5432'"

Ez a hibaüzenet megmutatja:

  • Az adatbázis belső host nevét és portját
  • A felhasználónevet
  • Hogy PostgreSQL-t használnak
# ✅ Felhasználónak: általános hibaüzenet; logban: a részletek
@app.errorhandler(500)
def server_error(e):
    logger.error(f"Internal error: {str(e)}")  # fejlesztőknek
    return jsonify({"error": "Internal server error"}), 500  # felhasználónak

4. példa: A „csak fejlesztésre" mentalitás csapdája

# "Ezt csak fejlesztésre használom, majd production-ban megváltoztatom"
SECRET_KEY = "dev_secret"
DEBUG = True
DATABASE_URL = "postgresql://admin:admin@localhost/myapp"

Két probléma:

  1. A „majd production-ban" sokszor elmarad
  2. A fejlesztési szokások átvivődnek a production-ba

Jobb megközelítés: kezdettől fogva .env fájlból olvasni a konfigurációt.


Tesztfeladatok

1. feladat – Jelszótárolási módszerek rangsorolása

Sorold fel a következő jelszótárolási módszereket legveszélyesebbtől a legbiztonságosabbig:

  • A) bcrypt (cost factor 12)
  • B) SHA-256 só nélkül
  • C) Plaintext
  • D) MD5
  • E) argon2id

Helyes sorrend: C → D → B → A → E
(plaintext → md5 → sha256 sóz. nélkül → bcrypt → argon2)


2. feladat – Igaz vagy hamis?

Értékeld az alábbi állításokat:

a) „A HTTPS megvédi az adatbázisban tárolt jelszavakat."
b) „A salt (só) titkos érték, amelyet külön kell tárolni a hash-től."
c) „Ha az MD5 hash-t sóval egészítem ki, az megfelel production jelszótárolásra."
d) „A bcrypt szándékosan lassú – ez a tervezési cél, nem hiba."
e) „A .env.example fájl biztonságosan kerülhet Git-be."

Válaszok:
a) Hamis – HTTPS az átvitelt védi, nem a tárolást
b) Hamis – a só nem titok, tárolható a hash mellé
c) Hamis – MD5 alapvetően nem megfelelő jelszóhash-hez, sóval sem
d) Igaz – a lassúság véd a brute force ellen
e) Igaz – ha nem tartalmaz valódi értékeket, csak kulcs-neveket


3. feladat – Kódhibák azonosítása

Az alábbi kódrészletben jelöld meg az összes security problémát, és magyarázd el, mit kellene másképp csinálni:

import hashlib
import logging

@app.route('/login', methods=['POST'])
def login():
    email = request.form['email']
    password = request.form['password']
    
    # Jelszó ellenőrzése
    password_hash = hashlib.md5(password.encode()).hexdigest()
    user = User.query.filter_by(email=email, password=password_hash).first()
    
    if user:
        logging.info(f"Successful login: {email} / {password}")
        return jsonify({"token": generate_token(user)})
    else:
        return jsonify({"error": f"No user found with email {email}"}), 401

Megoldás – azonosítandó hibák:

  1. MD5 jelszóhash – lecserélendő bcrypt-re
  2. Jelszó a logban – soha ne logolj jelszót (sem plaintext, sem hash)
  3. Email a hibaüzenetben – ne áruld el, melyik email létezik/nem létezik (user enumeration)

4. feladat – .gitignore ellenőrzés

Az alábbi fájlok közül melyek nem kerülhetnek soha Git-be?

FájlGit-be kerülhet?
.env?
.env.example?
config.py (hardcoded API kulcsokkal)?
config.py (csak os.getenv() hívásokkal)?
secrets/production.json?
README.md (ahol le van írva a DB jelszó)?

Válaszok:

  • .envNEM (valódi értékeket tartalmaz)
  • .env.exampleIGEN (csak kulcsneveket tartalmaz)
  • config.py hardcoded kulcsokkal → NEM
  • config.py csak os.getenv() hívásokkal → IGEN
  • secrets/production.jsonNEM
  • README.md DB jelszóval → NEM

5. feladat – PII azonosítása

Az alábbi adatmezők közül melyek tekinthetők PII-nek?

  • A) Felhasználó neve (full_name)
  • B) Felhasználó e-mail címe
  • C) Anonymizált felhasználói ID (UUID)
  • D) Felhasználó születési dátuma
  • E) Egy blogbejegyzés publikálási dátuma
  • F) Felhasználó IP-címe
  • G) Termék neve az adatbázisban
  • H) Felhasználó telefonszáma

PII: A, B, D, F, H
Nem PII: C (ha nem kapcsolható személyhez), E, G


6. feladat – Jelszóhash működés megértése

Miért ad a bcrypt különböző hash-t ugyanarra a jelszóra, ha kétszer futtatod?

import bcrypt

password = "titkos123".encode()
hash1 = bcrypt.hashpw(password, bcrypt.gensalt())
hash2 = bcrypt.hashpw(password, bcrypt.gensalt())

print(hash1 == hash2)  # Mit ír ki és miért?

Válasz: False – mert minden gensalt() hívás új, véletlenszerű sót generál. A különböző só különböző hash-t eredményez. Ez szándékos: ezért nem lehet rainbow table-lel támadni. A bcrypt.checkpw() a hash-ben tárolt sót használja az összehasonlításhoz, így az ellenőrzés akkor is működik, ha a hash különböző.


7. feladat – Szituációs döntés

Az alábbi szituációkban mit tegyél? Válaszd a helyes megközelítést és indokold meg.

Szituáció A: Véletlenül commitoltad a .env fájlt, de még nem pusholtad.

  • 1. opció: git commit --amend a .env nélkül, majd push
  • 2. opcio: Törli a .env-t, új commitot csinál, push
  • 3. opcio: Visszavonja az API kulcsokat, majd git history tisztítása

Helyes: 3. opcio – a kulcsok visszavonása az első lépés, még ha nem is pusholtad, mert nem tudod, más látta-e. Utána history tisztítás.


Szituáció B: A senior fejlesztő azt mondja: „Egyelőre SHA-256-ot használunk jelszóra, majd ha lesz idő, átírjuk bcrypt-re."

  • 1. opcio: Elfogadod, megcsinálod SHA-256-tal
  • 2. opcio: Megkérdezed, mikor lesz ez az átírás, és kéred, hogy prioritást kapjon
  • 3. opcio: Csendben bcrypt-et implementálsz SHA-256 helyett

Helyes: 2. opcio – jelzed a kockázatot, de nem döntöd felül a seniort egyeztetés nélkül. A security kockázatot kommunikálni kell, nem elhallgatni.


8. feladat – Hibakeresés: mi a probléma?

Egy felhasználó jelenti: bejelentkezés után néhány perc múlva automatikusan kijelentkezik, és nem érti, miért. A fejlesztő megnézi a logot:

[INFO] User login: user_id=42, email=user@example.com, session_token=eyJhbGc...
[INFO] Session validated: token=eyJhbGc...
[INFO] Session expired: token=eyJhbGc...

Kérdés 1: Mi a security probléma a logban?
Kérdés 2: Mi lehet a bejelentkezési probléma oka?

Válaszok:

  1. A session token a logban jelenik meg – ha valaki hozzáfér a logfájlhoz, ellophatja a tokent és megszemélyesítheti a felhasználót. Logba csak az ID kerüljön, nem a teljes token.
  2. A session lejárati ideje valószínűleg túl rövid, vagy a token érvényesítési logika hibás.

9. feladat – Összefüggések megértése

Párosítsd össze a fogalmakat és definíciókat:

FogalomDefiníció
SaltA. Szándékosan lassú hash algoritmus jelszóhoz
bcryptB. Véletlenszerű érték, amelyet hash előtt a jelszóhoz adnak
Rainbow tableC. Előre kiszámolt hash → jelszó párok adatbázisa
PIID. Személyes azonosításra alkalmas adat
.env.exampleE. Konfigurációs kulcsokat dokumentáló, értékek nélküli sablon
HTTPSF. Az adatátvitelt titkosítja átvitel közben

Megoldás: Salt→B, bcrypt→A, Rainbow table→C, PII→D, .env.example→E, HTTPS→F


10. feladat – Gondolkodj fejlesztőként

Egy új featurét kapsz: a felhasználók exportálhatják az adataikat (GDPR-kompatibilis export). Az export tartalmazza: nevet, e-mail címet, összes posztot, regisztrációs dátumot.

Kérdés: Milyen security és privacy szempontokat kell figyelembe venni az implementációnál? Írj legalább 4 szempontot.

Lehetséges válaszok (minimum 4):

  1. Hitelesítés: Csak a saját adatait exportálhatja a felhasználó – ellenőrizni kell, hogy a kért export az aktuálisan bejelentkezett felhasználóhoz tartozik-e
  2. Rate limiting: Korlátozzuk, hányszor exportálhat egy felhasználó (hogy ne lehessen scraping eszközként használni)
  3. Nem kerülhet bele: jelszóhash, belső ID-k, más felhasználók adatai
  4. Letöltési link lejárata: Ha ideiglenes letöltési linket generálsz, legyen rövid lejáratú
  5. Logolás: Loggolj minden exportot (ki, mikor), de maga az export tartalma ne kerüljön logba
  6. HTTPS: Az export csak titkosított kapcsolaton töltődhet le
Scroll to Top