JSON Web Token Biztonság: Útmutató a Feltörhetetlen Hitelesítéshez

Tematika
Biztonság
JavaScript
Node
WebFejlesztés
Közzétéve
2023. február 19.
Olvasási idő
9 Perc
Cikk megosztása
JSON Web Token Biztonság: Útmutató a Feltörhetetlen Hitelesítéshez
JSON Web Token Biztonság: Útmutató a Feltörhetetlen Hitelesítéshez
A modern webalkalmazások hitelesítési problémái nem újkeletűek. Az internet korai éveiben a munkamenetek kezelésére szerver oldalon tárolt session-öket használtak, amelyek egy egyszerű, cookie-ban elhelyezett azonosítóhoz kötötték a felhasználó állapotát. Ez a megközelítés évtizedekig működött, ám a felhőalapú architektúrák, a mikroszolgáltatások és a horizontálisan skálázható rendszerek megjelenésével egyre komolyabb gondokat okozott. Ha egy alkalmazás tucatnyi szerverpéldányon fut párhuzamosan, a session-adatok megosztása ezek között komplex infrastruktúrát igényel. A JSON Web Token erre a kihívásra kínál elegáns megoldást.

A JWT-k lényege, hogy az állapotinformációt nem a szerveren tárolják, hanem magában a tokenben, amelyet a kliens kap meg és küld vissza minden egyes kéréssel. A szerver ezért bármely példányon képes ellenőrizni a token hitelességét, pusztán a titkos kulcs és a token tartalma alapján, megosztott session-tárolás nélkül. Ez teszi a JWT-t ideálissá az elosztott, skálázható rendszerek számára. Azonban ez az önálló, stateless természet egyszerre a legerősebb és legveszélyesebb tulajdonsága. Ha egyszer egy token illetéktelen kezekbe kerül, a szerver nem tud mit tenni ellene egészen addig, amíg le nem jár.

TL;DR – A legfontosabb biztonsági lépések

  • Soha ne tárolj JWT-t LocalStorage-ban! Használj HttpOnly, Secure és SameSite sütiket az XSS elleni védelemhez.
  • Rövid lejárati idő + Refresh Token: Az access token legyen max. 15-60 perc, a hosszú távú elérést külön adatbázisban tárolt refresh tokennel kezeld.
  • Használj aszimmetrikus aláírást (RS256): Biztonságosabb, és lehetővé teszi a tokenek ellenőrzését a privát kulcs megosztása nélkül.
  • Implementálj ujjlenyomatot (Fingerprint): Kösd a tokent a kliens IP-címéhez és User-Agentjéhez a visszaélések minimalizálására.
  • Ne bízz az algoritmusban: Mindig rögzítsd a szerveroldalon, hogy milyen algoritmust fogadsz el, és tiltsd le a none típust.

A JWT anatómiája

Mielőtt a biztonsági szempontokba mélyednénk, érdemes megérteni, pontosan mivel is állunk szemben. Egy JWT három részből áll, amelyeket ponttal (.) választanak el egymástól: a fejlécből (header), a törzsből (payload) és az aláírásból (signature). Mindhárom rész Base64URL-kódolással van átalakítva, ami azt jelenti, hogy tartalmuk — a fejléc és a törzs esetében — szabadon dekódolható és olvasható. Az aláírás funkciója nem az adatok titkosítása, hanem azok integritásának védelme.

A fejléc tipikusan két dolgot tartalmaz: a token típusát (mindig JWT) és az aláíráshoz használt algoritmust, például HS256 (HMAC-SHA256) vagy RS256 (RSA-SHA256). A törzs tartalmazza az ún. claimeket, vagyis az állításokat — ezek lehetnek szabványosított mezők, mint az iss (kibocsátó), sub (alany), exp (lejárat), iat (kibocsátás időpontja), vagy tetszőleges egyéni adatok. Az aláírást a fejléc és a törzs Base64URL-kódolt változatának összefűzéséből, a titkos kulcs segítségével állítják elő a választott algoritmussal.

Ez a hármas struktúra határozza meg a JWT alapvető biztonsági modelljét is. Az aláírás garantálja, hogy a token tartalma nem változott meg az útja során, ám semmit nem mond arról, hogy ki olvasta el.

A leggyakoribb JWT sebezhetőségek

Mielőtt a védekezési stratégiákra rátérnénk, meg kell értenünk, milyen támadási vektorok fenyegetik a JWT-ket a gyakorlatban. Ezek ismerete nélkül a biztonsági intézkedések csupán egy hiányos puzzle részei maradnak.

Az egyik leghíresebb, és talán legmegdöbbentőbb sérülékenység az ún. "none" algoritmus támadás. A JWT szabvány eredetileg lehetővé tette, hogy a fejlécben az algoritmus értéke none legyen, jelezve, hogy a token nem aláírt. Számos korai és rosszul implementált könyvtár ezt elfogadta, és ellenőrzés nélkül hitelesítette az ilyen tokeneket. Egy támadó tehát egyszerűen módosíthatta a token törzsét (például magasabb jogosultságokat adva magának), majd az algoritmust none-ra állítva és az aláírást elhagyva, érvényes tokennek álcázta a hamisítványt.

Az algoritmuscsere-támadás (algorithm confusion attack) egy kifinomultabb változat, amely elsősorban az aszimmetrikus kulcspárokat (RS256) használó rendszereket célozza. Ha egy szerver az RS256 algoritmust is és a HS256-ot is elfogadja, egy támadó megpróbálhatja a nyilvános kulcsot — amely definíció szerint nem titkos — HS256 titkos kulcsként felhasználni a token aláírásához. A rosszul konfigurált szerver ezt érvényesnek fogadhatja el.

A brute force és szótártámadások szintén komoly fenyegetést jelentenek, különösen akkor, ha a fejlesztők gyenge, kiszámítható titkos kulcsokat használnak, mint például secret, password vagy a projekt neve. Mivel a JWT formátuma nyilvános, egy támadó offline, a szerver terhelése nélkül kísérelheti meg a kulcs kitalálását, ha egyszer megszerez egy érvényes tokent.

Nem szabad megfeledkezni a token eltulajdonítás (token theft) problémájáról sem. Ha egy JWT-t nem megfelelő helyen tárolnak a kliensen — erről később részletesen lesz szó —, egy XSS (Cross-Site Scripting) támadás segítségével a rosszindulatú kód egyszerűen kiolvashatja és egy idegen szerverre küldheti a tokent. A szerver szempontjából ez a lekérdezés teljesen legitim, hiszen a token érvényes.

Alapvető biztonsági intézkedések

Az első és legfontosabb szabály: soha ne közöljük JWT-ket titkosítatlan csatornán. Minden JWT-t HTTPS kapcsolaton keresztül kell továbbítani érvényes TLS tanúsítvánnyal. Ez biztosítja, hogy a kliens és a szerver között utazó adatokat ne lehessen lehallgatni. A HTTP-n küldött tokenek egy man-in-the-middle támadás könnyű zsákmányai, és az ilyen kommunikációban semmi nem akadályozza meg a token megszerzését.

A titkos kulcs biztonsága alapvető fontosságú. A HMAC-alapú aláírásnál (pl. HS256) a kulcsnak kriptográfiailag erősnek, véletlenszerűen generáltnak és legalább 256 bit hosszúnak kell lennie. Sosem szabad kézzel kitalált, rövid, vagy emberek által könnyen megjegyezhető kulcsot használni. A kulcsot soha nem szabad a forráskódba égetni. A verziókövető rendszerbe (pl. Git) bekerült titkos kulcs kompromittálódottnak tekintendő, és azonnal le kell cserélni. Ehelyett környezeti változókat, titkos tároló rendszereket (pl. HashiCorp Vault, AWS Secrets Manager) kell alkalmazni.

Lehetőség szerint érdemes aszimmetrikus kulcspárokat (RS256 vagy ES256) alkalmazni a szimmetrikus HS256 helyett. Aszimmetrikus esetben a token aláírásához privát kulcsot, ellenőrzéséhez pedig a nyilvános kulcsot használják. Ez lehetővé teszi, hogy különböző szolgáltatások ellenőrizhessék a tokeneket anélkül, hogy hozzáférnének az aláíráshoz szükséges titkos kulcshoz, ami egy mikroszolgáltatásos architektúrában különösen értékes tulajdonság.

A JWT törzsében sosem szabad érzékeny adatokat tárolni. Jelszavak, bankkártyaszámok, személyi azonosítók, vagy más bizalmas információk nem kerülhetnek a tokenbe. A törzs tartalma Base64URL-kódolt, ami könnyen visszafejthetővé teszi, így bárki, aki hozzáfér a tokenhez, el is tudja olvasni a tartalmát. A tokenben csak olyan adatok helyezhetők el, amelyek nyilvánosak lehetnek, mint például a felhasználói azonosító vagy a jogosultsági szerepkörök.

Az aláírás és a lejárati idő ellenőrzése nem opcionális, hanem kötelező minden egyes kérésnél. A exp (expiration) claim ellenőrzése garantálja, hogy lejárt tokennel ne lehessen hozzáférni a rendszerhez. Az iat (issued at) claim segítségével meghatározható, hogy egy token nem régebbi-e egy elfogadható küszöbértéknél, ami véd a régi, esetleg kompromittálódott tokenek újrahasználata ellen.

A token tárolása a kliensen

A JWT tárolásának helye a böngészőben az egyik legtöbbet vitatott témakör a webbiztonsági közösségben, és a döntés messzemenő következményekkel jár.

Tárolási módXSS elleni védelemCSRF elleni védelemHozzáférés JS-ből
LocalStorage❌ Nincs✅ Automatikus✅ Igen
HttpOnly Cookie✅ Teljes⚠️ SameSite-ot igényel❌ Nem

A localStorage kényelmes és elterjedt tárolási hely, ám súlyos biztonsági kockázatot hordoz. A localStorage-ban tárolt adatok bármely JavaScript kódból hozzáférhetők, amely az oldalon fut, ami azt jelenti, hogy egy XSS sebezhetőség esetén a támadó kódja egyszerűen kiolvashatja a tokent és elküldheti egy tetszőleges szerverre. Az XSS-t teljes mértékben eliminálni rendkívül nehéz, így a localStorage-os token tárolás elfogadhatatlan kockázatot jelent.

A biztonságosabb alternatíva a HttpOnly jelzővel ellátott süti (cookie). Az ilyen sütiket a böngésző automatikusan minden kéréssel elküldi a megfelelő domainre, azonban JavaScript kódból nem férhetők hozzá, ezáltal teljes védelmet nyújt az XSS-alapú lopás ellen. A Secure jelző biztosítja, hogy a süti csak HTTPS kapcsolaton utazzon, a SameSite=Strict vagy SameSite=Lax beállítás pedig a CSRF (Cross-Site Request Forgery) támadások ellen ad védelmet. Ez a megközelítés nem tökéletes, mivel a CSRF elleni védelmet külön is meg kell szervezni, de összességében biztonságosabb, mint a localStorage.

Fejlett biztonsági intézkedések

Az alapvető intézkedések ellenére a JWT-k strukturális gyengesége megmarad: amint egy token kibocsátásra kerül, visszavonni rendkívül nehéz. A token érvényes marad egészen a lejáratáig, még akkor is, ha a felhasználó kijelentkezett, vagy ha a token kompromittálódott. Ezt a problémát különböző stratégiákkal lehet kezelni.

Az egyik leghatékonyabb megközelítés a rövid lejárati idő kombinálása a frissítő token (refresh token) mechanizmussal. Az access token lejáratát néhány percre vagy legfeljebb egy-két órára érdemes beállítani. Amikor az access token lejár, a kliens egy hosszabb életű refresh tokennel tud új access tokent igényelni a szerver dedikált végpontján. A refresh tokeneket a szerveren (adatbázisban) kell tárolni és nyilván kell tartani, ami lehetővé teszi a visszavonásukat. Ha a felhasználó kijelentkezik, vagy gyanús tevékenységet észlelünk, a refresh tokent töröljük, így a következő access token megújításkori kísérlet meghiúsul.

A token visszavonási lista (token denylist vagy blocklist) egy másik megközelítés. Amikor egy tokent érvényteleníteni kell (pl. kijelentkezéskor), a token azonosítóját (jti claim) be kell jegyezni egy gyors elérésű tárolóba (pl. Redis). Minden kérésnél ellenőrizni kell, hogy a token jti értéke szerepel-e a listán. Ez a megközelítés visszaállítja a session-alapú rendszerek rugalmasságát, ám részben feladja a stateless JWT egyik előnyét. A denylist méretét az access tokenek rövid lejáratával lehet kezelhető szinten tartani.

A kliens ujjlenyomat (fingerprint) alapú ellenőrzés egy további biztonsági réteget ad, amely megnehezíti az ellopott tokenek felhasználását. Az ötlet lényege, hogy a token kibocsátásakor rögzítjük a kliens bizonyos jellemzőit, mint például a hálózati cím és a User-Agent karakterlánc kombinációját, és ezt egy hash formájában a tokenbe zárjuk. Minden kérésnél újra kiszámítjuk az ujjlenyomatot, és összevetjük a tokenben tároltal. Ha eltérés mutatkozik, a kérést elutasítjuk. Ez a módszer nem nyújt teljes védelmet, mivel az IP-cím és a User-Agent hamisítható, de jelentősen megemeli a támadás komplexitását.

Példakód

Az alábbiakban bemutatunk egy Express.js alapú implementációt, amely ötvözi a rövid lejáratú access tokent a fingerprint-alapú ellenőrzéssel. A logika természetesen tovább bővíthető és a konkrét alkalmazás igényeihez igazítható.

A teljes forráskód elérhető GitHub-repozitóriumunkban is:

https://github.com/stacklegend/jwt-improve-security

Az ujjlenyomat létrehozása

Az ujjlenyomatot egy sóval (salt) kiegészített, kriptográfiai hash-függvénnyel állítjuk elő. A só megakadályozza az előre kiszámított (rainbow table) támadásokat, és egyedivé teszi az ujjlenyomatot az adott alkalmazásra nézve.

const createFingerprint = (req) => {
  const salt = '3d4bd49fadee0613cec5a145a0173876';
  const addr = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
  const userAgent = req.get('user-agent');
  return crypto
    .createHash('sha256')
    .update(salt + addr + userAgent)
    .digest('hex');
};

A token kibocsátása és ellenőrzése

const crypto = require('crypto');
const router = require('express').Router();
const jwt = require('jsonwebtoken');

const secret = process.env.JWT_SECRET;
const expiresIn = '15m';

const createFingerprint = (req) => {
  const salt = process.env.FINGERPRINT_SALT;
  const addr = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
  const userAgent = req.get('user-agent');
  return crypto
    .createHash('sha256')
    .update(salt + addr + userAgent)
    .digest('hex');
};

router.post('/getToken', function (req, res) {
  const userId = req.body.userId;
  const fingerprint = createFingerprint(req);
  const data = { userId, fingerprint };
  const token = jwt.sign(data, secret, { expiresIn });

  res.json({ token });
});

router.post('/checkToken', function (req, res) {
  const token = req.headers['authorization']?.split(' ')[1];
  const fingerprint = createFingerprint(req);

  if (!token) return res.sendStatus(401);

  try {
    const data = jwt.verify(token, secret);

    if (!data || String(data.fingerprint) !== String(fingerprint)) {
      return res.sendStatus(403);
    }

    res.json({ data });
  } catch (err) {
    return res.sendStatus(403);
  }
});

module.exports = router;

Bevált titkos kulcs generálás

Éles környezetben soha ne kézzel írjuk be a titkos kulcsot. Egy megfelelően erős kulcsot Node.js-ben egyszerűen generálhatunk:

const crypto = require('crypto');
console.log(crypto.randomBytes(64).toString('hex'));

Az így kapott 512 bites, véletlenszerű kulcsot a szerver indításakor egy környezeti változóból kell beolvasni, és soha nem szabad verziókövető rendszerbe commitolni.

A kulcsrotáció fontossága

Egy sokak által figyelmen kívül hagyott, mégis kritikus fontosságú biztonsági gyakorlat a titkos kulcsok rendszeres cseréje (key rotation). Még ha a kulcs nem is kompromittálódott, az idő múlásával nő az esélye, hogy valahol valamilyen biztonsági incidensben érintett lehet. A kulcsrotáció megvalósítása JWT-k esetén nem triviális, mivel a forgalomban lévő, a régi kulccsal aláírt tokeneket még el kell fogadni egészen lejáratukig.

Ezt a problémát az ún. kid (key ID) claim segítségével oldják meg elegánsan. A token fejlécébe belekerül annak a kulcsnak az azonosítója, amellyel aláírták. A szerver egy kulcs-nyilvántartásból (JWKS, JSON Web Key Set) keresi ki az ellenőrzéshez szükséges kulcsot. Így egyszerre több kulcs is érvényes lehet, az újabb tokenek az új kulccsal kerülnek aláírásra, a régiek pedig a régi kulccsal ellenőrizhetők egészen lejáratukig.

JWT az OAuth 2.0 és OpenID Connect ökoszisztémában

A JWT-ket a valós alkalmazásokban ritkán használják önállóan, leggyakrabban az OAuth 2.0 és az OpenID Connect (OIDC) protokollok keretein belül találkozunk velük. Az OAuth 2.0 egy felhatalmazási keretrendszer, amely lehetővé teszi, hogy egy alkalmazás korlátozott hozzáférést kapjon egy felhasználó erőforrásaihoz annak nevében, anélkül hogy a jelszavát megkapná. Az OpenID Connect erre épít, és identitás-réteget ad hozzá, lehetővé téve a hitelesítést is.

Ezekben a kontextusokban a JWT-k az access tokenek, ID tokenek és refresh tokenek hordozójaként működnek. Az access token hozzáférést ad a védett erőforrásokhoz, az ID token a felhasználó személyazonosságát igazolja, a refresh token pedig az előző kettő megújítását teszi lehetővé. Az OIDC által definiált, szabványos claim-mezők (pl. sub, iss, aud) egységes, átjárható hitelesítési élményt biztosítanak, amelyet könyvtárak és identitásszolgáltatók (pl. Auth0, Keycloak, Firebase) széles köre támogat.

Ha lehetséges, célszerű ilyen bevált, auditált identitásszolgáltatóra támaszkodni a JWT kezelésben, ahelyett, hogy mindent saját kézzel implementálnánk. A saját implementáció kockázatos, mert a fent ismertetett sebezhetőségek mindegyike könnyen előkerülhet egy apró figyelmetlenség hatására.

Naplózás és anomália-detekció

A legbiztonságosabb rendszer sem makulátlan, ezért elengedhetetlen a folyamatos megfigyelés. A hitelesítési eseményeket — sikeres és sikertelen kísérleteket egyaránt — naplózni kell, és az adatokat elemezni. Szokatlanul sok sikertelen hitelesítés egy adott IP-ről jelszó-próbálgatási (brute force) támadásra utalhat. Ha egy felhasználó egyszerre több, egymástól távoli helyről jelenik meg, az token lopás jele lehet.

Az anomália-detekció automatizálható, például egy egyszerű szabályrendszer alapján le lehet tiltani az IP-t, értesíteni a felhasználót, vagy kényszerített újrahitelesítést igényelni. Ezek az intézkedések a technológiai biztonsági rétegeken felüli, operatív védelmet nyújtanak, és a valós incidensek felderítésének elsődleges eszközei.

Következtetés

A JWT-k hatékony és rugalmas eszközök a modern webalkalmazások hitelesítési és felhatalmazási feladatainak megoldására, különösen elosztott, mikroszolgáltatásokon alapuló rendszerekben, ahol a szerver oldalon tárolt session nem praktikus. Ám ez a rugalmasság felelősséggel jár. A JWT önálló, hordozható jellege azt jelenti, hogy az összes biztonsági követelmény betartása teljes egészében az implementáló fejlesztő felelőssége.

Az erős, véletlenszerűen generált kulcs, a HTTPS kizárólagos használata, a rövid lejárati idő, a refresh token mechanizmus, a kliens-ujjlenyomat alapú ellenőrzés és a megfelelő tárolási stratégia együttesen olyan védelmi réteget alkotnak, amely jelentősen megnehezíti a tokenek sikeres kihasználását. Egyetlen intézkedés sem elegendő önmagában, a biztonság mindig réteges, és az egyes rétegek egymás gyengeségeit kompenzálják.


Iratkozz fel hírlevelünkre

Maradj naprakész a legfrissebb hírekkel és betekintésekkel csapatunktól.

Add meg neved és email címed a feliratkozáshoz