05.12.2025

Wie ich einen kritischen Auth-Bypass in 2 Stunden fand

Auth Bypass Case Study Pentest Web Security

Das Setup

Eine B2B-SaaS-Plattform. Mandantenfähig. React-Frontend, Node.js-Backend, JWT zur Authentifizierung. Ich startete das Engagement um 9 Uhr. Um 11 Uhr durchstöberte ich das Admin-Panel als Superadmin.

Kein Sourcecode-Zugang. Reiner Black-Box-Test.

Schritt 1: Die Rollen verstehen

Die Anwendung hatte drei sichtbare Nutzerrollen (aus Signup und API-Responses):

  • Viewer — lesender Zugriff auf zugewiesene Projekte
  • Member — kann Projekte bearbeiten, Nutzer einladen
  • Admin — verwaltet den Mandanten: Abrechnung, Nutzer, Einstellungen
  • Aber das JWT-Payload erzählte eine andere Geschichte:

    {
      "sub": "user_abc123",
      "tenant": "tenant_xyz",
      "role": "member",
      "iat": 1701432000,
      "exp": 1701518400
    }

    Das role-Feld war ein String. Was passiert, wenn wir es ändern?

    Schritt 2: Rollen-Eskalation testen

    Das JWT war mit HS256 signiert. Ohne das Secret konnte ich keine Tokens fälschen. Aber ich konnte testen, was mit einem modifizierten Token passiert (der Server würde es ablehnen — oder etwa nicht?).

    Ich fing einen Request an /api/admin/users ab und entfernte den Authorization-Header komplett.

    403 Forbidden. Erwartet.

    Ich sendete ihn mit einem gültigen Benutzer-JWT (role: member).

    200 OK. Vollständige Admin-Benutzerliste zurückgegeben.

    Der Endpunkt prüfte auf irgendein gültiges JWT — aber nicht den Role-Claim darin. Das Einzige zwischen einem normalen Nutzer und der Admin-API war das Frontend, das die Admin-Navigationslinks ausblendete.

    Schritt 3: Jeden Admin-Endpunkt aufzählen

    Sobald ich das Muster kannte, überprüfte ich gängige Admin-Routen:

    GET  /api/admin/users          → 200 (alle Nutzer über alle Mandanten)
    GET  /api/admin/billing        → 200 (Abodaten, Umsätze)
    POST /api/admin/users/invite   → 200 (neue Admin-Nutzer einladen)
    GET  /api/admin/settings       → 200 (SMTP-Konfiguration, API-Keys)
    POST /api/admin/settings       → 200 (könnte SMTP ändern, um E-Mails abzufangen)

    Jeder einzelne Admin-Endpunkt war durch Authentifizierung geschützt (gültiges JWT erforderlich) — aber null Authorization (keine Rollen- oder Berechtigungsprüfung).

    Die Ursache

    Das Team hatte die JWT-Verifikation als globale Middleware implementiert:

    // middleware/auth.js
    const jwt = require('jsonwebtoken');
    
    

    module.exports = (req, res, next) => { const token = req.headers.authorization?.split(' ')[1]; if (!token) return res.status(401).send('Unauthorized');

    try { req.user = jwt.verify(token, process.env.JWT_SECRET); next(); // <-- Jedes gültige Token passiert. Keine Rollenprüfung. } catch { res.status(401).send('Invalid token'); } };

    Und die Admin-Routen verwendeten dieselbe Middleware ohne zusätzliche Prüfungen:

    // routes/admin.js
    router.use(authMiddleware); // Prüft, ob JWT existiert & gültig ist
    router.get('/users', (req, res) => {
      // Keine Prüfung, ob req.user.role === 'admin'
      const users = await db.getAllUsers();
      res.json(users);
    });

    Der Fix

    Zwei Änderungen:

  • Rollenbasierte Middleware — eine zweite Middleware, die req.user.role gegen die erforderliche Rolle prüft:
  • const requireRole = (...roles) => (req, res, next) => {
      if (!roles.includes(req.user.role)) {
        return res.status(403).send('Forbidden');
      }
      next();
    };
    
    

    router.use(authMiddleware); router.use(requireRole('admin', 'superadmin')); router.get('/users', (req, res) => { ... });

  • Mandanten-bezogene Queries — selbst Admin-Nutzer sollten nur die Daten ihres eigenen Mandanten sehen:
  • const users = await db.getUsersByTenant(req.user.tenant_id);

    Lessons Learned

    • Vertraue nicht dem Frontend, Autorisierung durchzusetzen. Admin-Links in der UI auszublenden ist keine Sicherheit. Angreifer nutzen nicht deine UI — sie senden Requests direkt.
    • Autorisierung ist pro Endpunkt, nicht pro Session. Eine gültige Session bedeutet nicht, dass der Nutzer auf jeden Endpunkt zugreifen darf.
    • Teste immer zuerst mit der niedrigsten Rolle. Ich starte einen Black-Box-Test immer als „Viewer" — die schwächste Rolle. Wenn ich mich von dort aus hocharbeiten kann, hast du ein Problem.
    ---

    Dieser Bug brauchte 2 Stunden zum Finden und Dokumentieren. Ein echter Angreifer hätte etwa 10 Minuten zum Ausnutzen gebraucht. Der Fix kostete das Entwicklerteam etwa 4 Stunden. Die Kosten ohne Test: ein meldepflichtiger Datenschutzverstoß nach DSGVO bei über 50 Geschäftskunden.

    ← Zurück zum Blog