Warum dein JWT-Setup im Backend trotz Library unsicher ist
March 17, 2026Deine JWT-Library ist korrekt implementiert. Trotzdem ist dein Setup unsicher. Ich weiß -- das klingt provokant. Du hast jsonwebtoken oder PyJWT installiert, die Doku gelesen, Tokens werden generiert und validiert. Tests sind grün. Ship it. Aber hier ist die Sache: Die Library selbst ist selten das Problem. Das Problem ist, was du drumherum nicht konfigurierst. Und genau das sehe ich immer wieder -- sowohl wenn ich Backends entwickle als auch wenn ich sie als Pentester auseinandernehme. In diesem Artikel zeige ich dir die fünf häufigsten JWT-Fehler, die ich in Audits finde. Für jeden Fehler bekommst du zuerst die Angreifer-Perspektive (wie ich das ausnutze) und dann den Fix (wie du es richtig machst). Denn genau das ist der Punkt: Wenn du nur die Entwickler-Brille aufsetzt, siehst du ein funktionierendes Token. Wenn du die Angreifer-Brille aufsetzt, siehst du eine Angriffsfläche.1. Der alg:none-Angriff -- Wenn dein Backend jeden Algorithmus akzeptiert
So sieht der Angriff aus
Json
{
"alg": "HS256",
"typ": "JWT"
}
Python
import base64
import json
# Header: Algorithmus auf "none" setzen
header = base64.urlsafe_b64encode(
json.dumps({"alg": "none", "typ": "JWT"}).encode()
).rstrip(b"=")
# Payload: Admin-Rechte geben wir uns selbst
payload = base64.urlsafe_b64encode(
json.dumps({"sub": "1", "role": "admin", "exp": 9999999999}).encode()
).rstrip(b"=")
# Signatur: einfach leer lassen
forged_token = f"{header.decode()}.{payload.decode()}."
print(forged_token)
Der Fix
Javascript
const jwt = require('jsonwebtoken');
// FALSCH: Algorithmus wird aus dem Token gelesen
const decoded = jwt.verify(token, secret);
// RICHTIG: Algorithmus wird serverseitig erzwungen
const decoded = jwt.verify(token, secret, {
algorithms: ['HS256'] // NUR diesen Algorithmus akzeptieren
});
Python
import jwt
# FALSCH
decoded = jwt.decode(token, secret, algorithms=["HS256", "none"])
# RICHTIG
decoded = jwt.decode(token, secret, algorithms=["HS256"])
2. Schwache oder symmetrische Secrets in Produktion
So sieht der Angriff aus
Bash
# hashcat im JWT-Modus
hashcat -a 0 -m 16500 jwt_token.txt wordlist.txt
# Oder mit dem spezialisierten Tool
jwt-cracker -t eyJhbGciOiJIUzI1NiIs... -d wordlist.txt
Der Fix
Javascript
const crypto = require('crypto');
// Generiere ein kryptografisch sicheres Secret
// Mindestens 256 Bit (32 Bytes) für HS256
const secret = crypto.randomBytes(64).toString('hex');
// Ergebnis: "a3f8b2c1d4e5..." (128 Zeichen Hex)
Javascript
const jwt = require('jsonwebtoken');
const fs = require('fs');
const privateKey = fs.readFileSync('./keys/private.pem');
const publicKey = fs.readFileSync('./keys/public.pem');
// Signieren mit Private Key
const token = jwt.sign({ sub: userId, role: 'user' }, privateKey, {
algorithm: 'RS256',
expiresIn: '15m'
});
// Verifizieren mit Public Key
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256'] // Algorithmus erzwingen!
});
3. Fehlende oder zu lange Token-Laufzeiten
So sieht der Angriff aus
Python
import jwt
import base64
import json
# Token aus dem LocalStorage der Zielanwendung extrahiert
stolen_token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0Iiwicm9sZSI6ImFkbWluIiwiZXhwIjoxNzc3NTk5NjAwfQ.xxxxx"
# Wann läuft das ab?
payload = json.loads(
base64.urlsafe_b64decode(stolen_token.split('.')[1] + '==')
)
from datetime import datetime
print(f"Expires: {datetime.fromtimestamp(payload['exp'])}")
# Ausgabe: Expires: 2026-06-28 12:00:00
# ...drei Monate. Wunderbar (für den Angreifer).
Der Fix
Javascript
const jwt = require('jsonwebtoken');
// Access Token: kurze Laufzeit (15 Minuten)
const accessToken = jwt.sign(
{ sub: userId, role: userRole },
privateKey,
{
algorithm: 'RS256',
expiresIn: '15m' // 15 Minuten, nicht 15 Tage
}
);
// Refresh Token: längere Laufzeit, aber in der DB gespeichert
// und damit widerrufbar
const refreshToken = jwt.sign(
{ sub: userId, type: 'refresh' },
refreshPrivateKey,
{
algorithm: 'RS256',
expiresIn: '7d'
}
);
Python
import jwt
# FALSCH: Expiration-Prüfung deaktiviert
decoded = jwt.decode(token, secret, algorithms=["HS256"],
options={"verify_exp": False}) # NIE in Produktion
# RICHTIG: Standard-Verhalten beibehalten
decoded = jwt.decode(token, secret, algorithms=["HS256"])
# Wirft jwt.ExpiredSignatureError wenn abgelaufen
4. Issuer und Audience werden nicht validiert
So sieht der Angriff aus
Python
# Token für die öffentliche API (völlig legitim)
api_token_payload = {
"sub": "user-123",
"role": "user",
"iss": "public-api",
"aud": "public-api",
"exp": 1742400000
}
# Der Admin-Service prüft iss/aud nicht?
# Dann akzeptiert er dieses Token auch. Privilege Escalation.
Der Fix
Javascript
// Beim Erstellen: Issuer und Audience setzen
const token = jwt.sign(
{ sub: userId, role: 'admin' },
privateKey,
{
algorithm: 'RS256',
expiresIn: '15m',
issuer: 'admin-service',
audience: 'admin-dashboard'
}
);
// Beim Validieren: Issuer und Audience erzwingen
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256'],
issuer: 'admin-service',
audience: 'admin-dashboard'
});
// Token von "public-api" wird jetzt abgelehnt
Python
# Python-Equivalent
decoded = jwt.decode(
token,
public_key,
algorithms=["RS256"],
audience="admin-dashboard",
issuer="admin-service"
)
5. Sensible Daten im JWT-Payload -- Base64 ist keine Verschlüsselung
So sieht der Angriff aus
Python
import base64
import json
token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0IiwiZW1haWwiOiJhZG1pbkBjb21wYW55LmNvbSIsInBhc3N3b3JkX2hhc2giOiIkMmIkMTIkYWJjZGVmIiwiaW50ZXJuYWxfaXAiOiIxMC4wLjEuNTAiLCJkYl9yb2xlIjoic3VwZXJ1c2VyIn0.xxxxx"
# Payload decodieren -- kein Key nötig!
payload_b64 = token.split('.')[1]
# Base64-Padding hinzufügen
payload_b64 += '=' * (4 - len(payload_b64) % 4)
payload = json.loads(base64.urlsafe_b64decode(payload_b64))
print(json.dumps(payload, indent=2))
Json
{
"sub": "1234",
"email": "admin@company.com",
"password_hash": "$2b$12$abcdef",
"internal_ip": "10.0.1.50",
"db_role": "superuser"
}
Der Fix
Javascript
// FALSCH: Sensible Daten im Token
const badToken = jwt.sign({
sub: userId,
email: user.email,
passwordHash: user.passwordHash, // Niemals!
internalIp: '10.0.1.50', // Niemals!
creditCard: user.creditCardLast4, // Niemals!
dbRole: 'superuser' // Niemals!
}, privateKey, { algorithm: 'RS256' });
// RICHTIG: Nur IDs und Rollen, alles andere aus der DB laden
const goodToken = jwt.sign({
sub: userId,
role: 'admin',
jti: crypto.randomUUID() // Unique Token ID für Revocation
}, privateKey, {
algorithm: 'RS256',
expiresIn: '15m',
issuer: 'auth-service',
audience: 'api'
});
Wann du JWT gar nicht verwenden solltest
- Du brauchst sofortige Invalidierung. Wenn ein User sein Passwort ändert oder ein Admin einen Account sperrt, muss der Zugriff sofort enden. JWTs sind stateless -- du kannst ein ausgegebenes Token nicht zurückrufen, ohne eine Blacklist zu führen. Und eine Blacklist ist im Grunde... eine Session-Datenbank.
- Deine Anwendung ist ein klassischer Monolith. Wenn du einen einzelnen Server hast, bringt JWT keinen Vorteil gegenüber Server-side Sessions. Sessions sind einfacher, sicherer (kein Token-Diebstahl aus dem Browser-Storage), und du bekommst Invalidierung gratis.
- Du speicherst JWTs im LocalStorage. LocalStorage ist für jeden JavaScript-Code auf der Seite zugänglich. Ein einziger XSS-Vulnerability -- und der Angreifer hat das Token. HttpOnly Cookies sind die sicherere Wahl für browser-basierte Anwendungen.
Javascript
// Wenn du schon JWTs im Browser brauchst:
// HttpOnly Cookie statt LocalStorage
res.cookie('access_token', token, {
httpOnly: true, // Kein Zugriff via JavaScript
secure: true, // Nur über HTTPS
sameSite: 'strict', // CSRF-Schutz
maxAge: 15 * 60 * 1000 // 15 Minuten
});
Die vollständige Checkliste
- [ ] Algorithmus wird serverseitig erzwungen (algorithms: ['RS256'])
- [ ] alg: none wird explizit abgelehnt
- [ ] Secret ist kryptografisch zufällig und mindestens 256 Bit lang (oder asymmetrische Keys)
- [ ] Access Tokens haben eine kurze Laufzeit (5-15 Minuten)
- [ ] Refresh Tokens sind in der Datenbank gespeichert und widerrufbar
- [ ] iss und aud Claims werden gesetzt und validiert
- [ ] Keine sensiblen Daten im Payload (keine E-Mails, Hashes, interne IPs)
- [ ] Tokens werden in HttpOnly Cookies gespeichert, nicht im LocalStorage
- [ ] HTTPS ist Pflicht (kein Token über unverschlüsselte Verbindungen)
- [ ] Es gibt einen Plan für Key Rotation
Fazit
Du willst wissen, ob dein JWT-Setup -- oder dein Backend generell -- einem echten Angriff standhält? Ich kombiniere Backend-Entwicklung mit Offensive Security und prüfe deine Systeme aus beiden Perspektiven. Schreib mir für ein Security Review oder einen Workshop: kontakt@buengener-software.de