Europe/Berlin
--:--:--

JWT-Sicherheit: 5 kritische Fehler, die dein API-Backend gefährden

April 3, 2026 Als Backend-Entwickler, der täglich mit API-Sicherheit zu tun hat, sehe ich immer wieder dieselben Fallstricke bei der Implementierung von JSON Web Tokens (JWTs). JWTs sind ein mächtiges Werkzeug, doch ihre vermeintliche Einfachheit führt oft zu Fehlern, die weitreichende Konsequenzen für die Sicherheit von Anwendungen haben können. Dieser Artikel beleuchtet die Grundlagen von JWTs und zeigt anhand praktischer Beispiele, wie man die häufigsten Sicherheitslücken schließt. JSON Web Tokens sind kompakte, URL-sichere Token, die entwickelt wurden, um Informationen zwischen Parteien sicher als JSON-Objekt zu übertragen. Sie werden häufig für Authentifizierung und Autorisierung in Microservices-Architekturen und APIs eingesetzt. Ein Client erhält ein JWT nach erfolgreicher Authentifizierung und sendet es bei jeder nachfolgenden Anfrage an das Backend, um seine Identität und Berechtigungen nachzuweisen. Ein JWT besteht aus drei Teilen, die durch Punkte voneinander getrennt sind:
  1. Header: Enthält Metadaten über den Token, typischerweise den Signaturalgorithmus (z.B. HS256 oder RS256) und den Typ des Tokens (JWT).
  2. Payload (Claims): Dies ist der eigentliche Datenblock. Hier werden sogenannte Claims gespeichert – Aussagen über eine Entität (normalerweise der Benutzer) und zusätzliche Metadaten. Beispiele sind die Benutzer-ID (sub), der Aussteller (iss), der Empfänger (aud), die Gültigkeitsdauer (exp) und die Rollen des Benutzers.
  3. Signatur: Eine kryptografische Signatur, die verwendet wird, um die Integrität des Tokens zu überprüfen. Sie wird aus dem kodierten Header, dem kodierten Payload und einem geheimen Schlüssel (oder einem privaten Schlüssel bei asymmetrischen Algorithmen) berechnet. Sie stellt sicher, dass der Token auf dem Transportweg nicht manipuliert wurde.
Alle drei Teile werden Base64-URL-kodiert und dann zu einem String zusammengefügt. Das Ergebnis ist ein Token, das typischerweise so aussieht: aaaaa.bbbbb.ccccc. In einer typischen Architektur meldet sich ein Benutzer an einem Authentifizierungsdienst an. Nach erfolgreicher Prüfung der Anmeldedaten erstellt dieser Dienst ein JWT und sendet es an den Client. Der Client speichert das Token und fügt es bei zukünftigen Anfragen in den Authorization-Header ein, meistens im Format Bearer <token>. Der Backend-Dienst, der die Anfrage empfängt, validiert das JWT, extrahiert die enthaltenen Claims und trifft basierend darauf Entscheidungen über Autorisierung. Ein häufiges Missverständnis ist, dass der Payload eines JWTs verschlüsselt sei. Das ist nicht der Fall! Der Payload ist lediglich Base64-URL-kodiert, was eine Kodierung, aber keine Verschlüsselung darstellt. Jeder, der das Token abfängt, kann den Payload dekodieren und seinen Inhalt lesen. Daher gilt die goldene Regel: Speichern Sie niemals sensible, personenbezogene oder vertrauliche Daten unverschlüsselt im JWT-Payload. Wenn solche Daten zwingend erforderlich sind, müssen diese vor dem Hinzufügen verschlüsselt oder Sie sollten stattdessen JSON Web Encryption (JWE) verwenden, das eine tatsächliche Verschlüsselung bietet. Die Signatur ist das Herzstück der JWT-Sicherheit. Sie schützt das Token vor Manipulation. Die Wahl des Signatur-Algorithmus ist entscheidend. Verwenden Sie stets starke Algorithmen wie HS256 (HMAC mit SHA-256) oder, noch besser, asymmetrische Algorithmen wie RS256 (RSA mit SHA-256), bei denen der private Schlüssel zum Signieren und der öffentliche Schlüssel zum Verifizieren verwendet wird. Der Geheimschlüssel für HS256 muss lang, zufällig und streng vertraulich behandelt werden. Ein kompromittierter Schlüssel macht alle damit signierten Tokens angreifbar. Kurze Gültigkeitsdauern (Claims wie exp für "expiration time") sind essenziell. Ein Token, das nur wenige Minuten gültig ist, minimiert das Risiko, dass ein gestohlenes Token über längere Zeit missbraucht werden kann. Dies ist ein Kompromiss zwischen Sicherheit und Usability, der oft durch die Kombination mit Refresh Tokens gelöst wird. Viele Entwickler konzentrieren sich nur auf die exp-Validierung, vergessen aber andere wichtige Claims. Eine unzureichende Claim-Validierung ist ein weit verbreiteter und kritischer Fehler. Ihr Backend muss alle relevanten Claims strikt überprüfen:
  • exp (Expiration Time): Das Token darf nicht abgelaufen sein.
  • nbf (Not Before): Das Token darf noch nicht gültig sein.
  • iss (Issuer): Prüfen Sie, ob der Aussteller des Tokens Ihr erwarteter Authentifizierungsdienst ist. Andernfalls könnten Tokens von anderen Diensten akzeptiert werden.
  • aud (Audience): Stellen Sie sicher, dass das Token für Ihre spezifische Anwendung oder Ihren Dienst bestimmt ist. Dies verhindert, dass ein Token, das für Anwendung A ausgestellt wurde, von Anwendung B akzeptiert wird.
  • sub (Subject): Die Identität des Principals, über den der Token Aussagen macht.
Ohne diese Prüfungen akzeptiert Ihr Dienst möglicherweise Tokens, die nicht für ihn bestimmt sind oder von nicht vertrauenswürdigen Quellen stammen. Ein besonders gefährlicher, aber leider immer wieder anzutreffender Fehler ist die none-Algorithmus-Schwachstelle. Einige ältere JWT-Bibliotheken oder fehlerhafte Konfigurationen erlauben es, JWTs mit dem Algorithmus none zu signieren. Dies bedeutet, dass überhaupt keine Signaturprüfung erfolgt. Ein Angreifer könnte ein Token manipulieren, den Header auf "alg":"none" setzen und ein solches Token würde von einem nachlässigen Backend akzeptiert, da die Signaturprüfung übergangen wird. Ihre JWT-Bibliothek muss die Verwendung des none-Algorithmus strikt ablehnen. JWTs sind per Design zustandslos. Das bedeutet, sobald sie ausgestellt sind, speichert der Server keine Informationen über ihre Gültigkeit mehr. Jede Validierung erfolgt rein auf Basis der im Token enthaltenen Informationen und der Signatur. Das macht die Widerrufung (Revocation) eines JWTs schwierig, beispielsweise bei einem Logout, einer Passwortänderung oder wenn ein Token kompromittiert wurde. Um die Herausforderung der Widerrufung zu meistern, gibt es mehrere Ansätze:
  1. Sehr kurze TTLs: Wenn Access Tokens nur wenige Minuten gültig sind, ist das Fenster für Missbrauch im Falle eines Diebstahls gering. Bei einem Logout wartet man einfach, bis das Token abläuft.
  2. Blocklisten (Blacklists): Man pflegt serverseitig eine Liste von kompromittierten oder explizit widerrufenen Token-IDs (JTI-Claim). Jede Anfrage mit einem JWT muss zusätzlich gegen diese Blockliste geprüft werden. Dies erfordert jedoch einen serverseitigen Zustand und kann die Performance beeinflussen, je nach Größe und Abfragefrequenz der Blockliste (oft in einem schnellen Cache wie Redis).
  3. Kombination mit Refresh Tokens: Eine bewährte Methode ist die Verwendung von kurzen Access Tokens (z.B. 5-15 Minuten) für den Zugriff auf Ressourcen und längeren Refresh Tokens (z.B. Tage oder Wochen) zum Anfordern neuer Access Tokens. Refresh Tokens werden serverseitig verwaltet und können dort jederzeit widerrufen werden. Sie werden typischerweise nur einmalig verwendet und dann durch ein neues Refresh Token ersetzt, um Reuse-Angriffe zu erschweren.
Die Art und Weise, wie JWTs im Browser des Benutzers gespeichert werden, ist ein entscheidender Sicherheitsfaktor. Hier sehe ich oft den gravierendsten Fehler: Das Speichern von JWTs im localStorage ist ein verbreiteter und kritischer Fehler. localStorage ist mittels JavaScript global zugänglich. Das bedeutet, dass jede Form von Cross-Site Scripting (XSS)-Angriff – sei es durch eine injizierte Schwachstelle in Ihrer eigenen Anwendung oder durch eine Drittanbieter-Bibliothek – einem Angreifer ermöglichen würde, das im localStorage gespeicherte Token einfach auszulesen und für eigene Zwecke zu missbrauchen. Der Angreifer kann dann im Namen des Benutzers API-Aufrufe tätigen. Bessere Alternativen:
  • HttpOnly-Cookies: Dies ist die bevorzugte Methode für traditionelle Webanwendungen. HttpOnly-Cookies können nicht über JavaScript gelesen oder manipuliert werden, was sie resistenter gegen XSS macht. Sie werden bei jeder Anfrage automatisch mitgesendet. Allerdings sind sie anfälliger für CSRF (Cross-Site Request Forgery), was durch zusätzliche CSRF-Token gemindert werden muss.
  • In-Memory-Speicher: Für Single Page Applications (SPAs) kann ein Access Token im Arbeitsspeicher des Browsers gespeichert werden. Dies bietet Schutz vor persistentem XSS, da das Token nach einem Seiten-Refresh (oder Browser-Neustart) verloren geht. Es erfordert jedoch, dass das Token häufig über ein Refresh Token neu angefordert wird.
Obwohl JWTs als "kompakt" beworben werden, können sie bei unsachgemäßer Befüllung zu Performance-Problemen führen. Vermeiden Sie es, zu viele oder zu große Daten in den JWT-Payload zu packen. Jede zusätzliche Information im Payload vergrößert das Token. Ein größeres Token bedeutet mehr Daten, die über die Leitung gesendet werden müssen – bei jeder einzelnen API-Anfrage. Dies erhöht den Netzwerk-Overhead und kann besonders in Architekturen mit vielen Microservices, die ein JWT bei jedem Aufruf weiterreichen, zu spürbaren Latenzen führen. Halten Sie den Payload auf das absolut Notwendigste beschränkt: Benutzer-ID, Rollen und kritische Claims wie exp, iss, aud. Weniger ist hier oft mehr.
Python
import jwt
import datetime

SECRET_KEY = "your-super-secret-key-that-is-strong-and-random-in-production"

def create_jwt(user_id: str) -> str:
    payload = {
        "user_id": user_id,
        "exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=30), # Ablauf in 30 Min
        "iat": datetime.datetime.utcnow(),
        "iss": "your-backend-service", # Aussteller
        "aud": "your-client-application" # Empfänger
    }
    return jwt.encode(payload, SECRET_KEY, algorithm="HS256")

def decode_jwt(token: str) -> dict | None:
    try:
        # Strikte Validierung von issuer und audience ist hier integriert
        decoded_payload = jwt.decode(
            token,
            SECRET_KEY,
            algorithms=["HS256"], # Erlaubte Algorithmen
            audience="your-client-application",
            issuer="your-backend-service"
        )
        return decoded_payload
    except (jwt.ExpiredSignatureError, jwt.InvalidTokenError, jwt.exceptions.InvalidAudienceError, jwt.exceptions.InvalidIssuerError) as e:
        print(f"JWT Fehler: {e}")
        return None

# Beispielnutzung:
token = create_jwt("user123")
print(f"Erzeugtes Token: {token}")

decoded_data = decode_jwt(token)
print(f"Dekodiertes Payload: {decoded_data}")

# Beispiel eines abgelaufenen Tokens (simuliert durch Manipulation des exp-Werts oder Wartezeit)
# decoded_data_expired = jwt.decode(token, SECRET_KEY, algorithms=["HS256"], audience="your-client-application", issuer="your-backend-service", options={"verify_exp": False})
# decoded_data_expired["exp"] = datetime.datetime.utcnow() - datetime.timedelta(minutes=1)
# expired_token = jwt.encode(decoded_data_expired, SECRET_KEY, algorithm="HS256") # Das ist KEIN sicherer Weg!
# print(f"Versuch dekodiert abgelaufen: {decode_jwt(expired_token)}")
Typescript
async function callAuthenticatedApi(url: string, jwtToken: string) {
  try {
    const response = await fetch(url, {
      method: 'GET',
      headers: {
        'Authorization': `Bearer ${jwtToken}`, // JWT im Authorization-Header
        'Content-Type': 'application/json'
      }
    });
    if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
    return await response.json();
  } catch (error) {
    console.error('Fehler beim API-Aufruf:', error);
    throw error;
  }
}

// Client-seitige Dekodierung (NUR zur Anzeige, NICHT zur Validierung!)
// Diese Funktion sollte niemals für Sicherheitsprüfungen verwendet werden.
// Die Validierung gehört IMMER ins Backend.
function decodeJwtPayload(token: string): any | null {
  try {
    const base64Url = token.split('.')[1];
    const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
    const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) {
      return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
    }).join(''));
    return JSON.parse(jsonPayload);
  } catch (e) {
    console.error('Fehler beim Dekodieren des JWT-Payloads:', e);
    return null;
  }
}

// Beispielaufruf (angenommen, das JWT kommt aus einem HttpOnly-Cookie oder In-Memory)
// const myJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidXNlcjEyMyIsImV4cCI6MTY3ODkwNTY3OCwiaWF0IjoxNjc4OTAyMDc4LCJpc3MiOiJ5b3VyLWJhY2tlbmQtc2VydmljZSIsImF1ZCI6InlvdXItY2xpZW50LWFwcGxpY2F0aW9uIn0.SIGNATUR_HIER";
// callAuthenticatedApi('https://api.example.com/data', myJwt)
//   .then(data => console.log('API-Antwort:', data))
//   .catch(err => console.error(err));

// const decodedClientInfo = decodeJwtPayload(myJwt);
// console.log('Client-seitig dekodierte Infos (nur Anzeige):', decodedClientInfo);
Für Debugging oder manuelle Überprüfungen in der Entwicklung oder im Betrieb ist es oft nützlich, ein JWT schnell von der Kommandozeile aus zu dekodieren. Denken Sie daran, dass dies nur den Header und Payload entschlüsselt, aber nicht die Signatur prüft.
Bash
YOUR_JWT_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"

echo "HEADER:"
echo "$YOUR_JWT_TOKEN" | awk -F'.' '{print $1}' | base64 --decode | jq '.'

echo "\nPAYLOAD:"
echo "$YOUR_JWT_TOKEN" | awk -F'.' '{print $2}' | base64 --decode | jq '.'

# Hinweis: Für die Signaturprüfung ist ein dediziertes Tool oder Code erforderlich.
# jq muss installiert sein (apt install jq / brew install jq)
Geheimnisse wie der SECRET_KEY dürfen niemals direkt in Images fest kodiert werden. Für Entwicklungsumgebungen können Umgebungsvariablen genutzt werden, in Produktion sollten jedoch sichere Secret-Management-Systeme zum Einsatz kommen.
Dockerfile
# Dockerfile Auszug für einen Backend-Dienst
FROM python:3.9-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install -r requirements.txt

COPY . .

# Umgebungsvariablen für Entwicklung. In Produktion NIE so setzen!
ENV JWT_SECRET="mySuperSecureDevelopmentSecret"
ENV JWT_ALGORITHM="HS256"
ENV JWT_EXPIRATION_MINUTES="30"

CMD ["python", "app.py"]
Yaml
# docker-compose.yml Auszug für Dev-Umgebung
services:
  backend:
    build: .
    ports:
      - "8000:8000"
    environment:
      JWT_SECRET: "mySuperSecureDevelopmentSecret"
      JWT_ALGORITHM: "HS256"
      JWT_EXPIRATION_MINUTES: "30"
    # In Produktion: secrets verwenden! z.B. Docker Secrets, Kubernetes Secrets oder HashiCorp Vault.
JWTs sind ein essenzieller Baustein moderner API-Sicherheit, aber ihre korrekte Implementierung erfordert tiefgreifendes Verständnis und Disziplin. Wer die genannten Fallstricke vermeidet, legt ein solides Fundament für ein sicheres Backend. Denken Sie immer daran: Sicherheit ist keine Funktion, die man nachrüstet, sondern ein integraler Bestandteil des Entwicklungsprozesses. Du brauchst Unterstützung bei der sicheren Implementierung von JWTs oder möchtest dein API-Backend auf Schwachstellen prüfen lassen? Für eine robuste und praxisnahe Security-Beratung stehe ich dir gerne zur Seite: Büngener Software