05.12.2025

Jak znalazłem krytyczny Auth Bypass w 2 godziny

Auth Bypass Case Study Pentest Web Security

Setup

Platforma B2B SaaS. Wielodzierżawcza (multi-tenant). React frontend, Node.js backend, JWT do autentykacji. Zacząłem zlecenie o 9 rano. O 11 przeglądałem panel admina jako superadmin.

Bez dostępu do kodu źródłowego. Czysty black-box.

Krok 1: Zrozumieć role

Aplikacja miała trzy widoczne role użytkowników (z rejestracji i odpowiedzi API):

  • Viewer — dostęp tylko do odczytu przypisanych projektów
  • Member — może edytować projekty, zapraszać użytkowników
  • Admin — zarządza tenantem: rozliczenia, użytkownicy, ustawienia
  • Ale payload JWT opowiadał inną historię:

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

    Pole role było stringiem. Co się stanie, jeśli je zmienimy?

    Krok 2: Przetestować eskalację roli

    JWT było podpisane HS256. Bez sekretu nie mogłem tworzyć tokenów. Ale mogłem przetestować, co się stanie ze zmodyfikowanym tokenem (serwer by go odrzucił — czy na pewno?).

    Przechwyciłem żądanie do /api/admin/users i całkowicie usunąłem nagłówek Authorization.

    403 Forbidden. Zgodnie z oczekiwaniami.

    Wysłałem je z poprawnym JWT użytkownika (role: member).

    200 OK. Zwrócona pełna lista użytkowników admina.

    Endpoint sprawdzał jakiekolwiek poprawne JWT — ale nie claim roli w jego wnętrzu. Jedyną rzeczą między zwykłym użytkownikiem a API admina było to, że frontend ukrywał linki nawigacyjne admina.

    Krok 3: Wyliczyć wszystkie endpointy admina

    Znając już wzorzec, sprawdziłem typowe trasy admina:

    GET  /api/admin/users          → 200 (wszyscy użytkownicy ze wszystkich tenantów)
    GET  /api/admin/billing        → 200 (dane subskrypcji, przychody)
    POST /api/admin/users/invite   → 200 (zapraszanie nowych adminów)
    GET  /api/admin/settings       → 200 (konfiguracja SMTP, klucze API)
    POST /api/admin/settings       → 200 (możliwość zmiany SMTP do przechwytywania maili)

    Każdy endpoint admina był chroniony autentykacją (wymagane poprawne JWT) — ale zerową autoryzacją (brak sprawdzenia roli lub uprawnień).

    Przyczyna źródłowa

    Zespół zaimplementował weryfikację JWT jako globalne middleware:

    // 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(); // <-- Każdy poprawny token przechodzi. Brak sprawdzenia roli. } catch { res.status(401).send('Invalid token'); } };

    A trasy admina używały tego samego middleware bez dodatkowych sprawdzeń:

    // routes/admin.js
    router.use(authMiddleware); // Sprawdza czy JWT istnieje i jest poprawny
    router.get('/users', (req, res) => {
      // Brak sprawdzenia, czy req.user.role === 'admin'
      const users = await db.getAllUsers();
      res.json(users);
    });

    Rozwiązanie

    Dwie zmiany:

  • Middleware oparte na rolach — drugie middleware sprawdzające req.user.role względem wymaganej roli:
  • 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) => { ... });

  • Zapytania ograniczone do tenanta — nawet admini powinni widzieć tylko dane swojego tenanta:
  • const users = await db.getUsersByTenant(req.user.tenant_id);

    Wnioski

    • Nie ufaj frontendowi w egzekwowaniu autoryzacji. Ukrywanie linków admina w UI to nie bezpieczeństwo. Atakujący nie używają Twojego UI — wysyłają żądania bezpośrednio.
    • Autoryzacja jest per-endpoint, nie per-sesja. Poprawna sesja nie oznacza, że użytkownik ma dostęp do każdego endpointu.
    • Zawsze zaczynaj test od roli z najniższymi uprawnieniami. Zawsze rozpoczynam test black-box jako „Viewer" — najsłabsza rola. Jeśli mogę eskalować z tego poziomu, masz problem.
    ---

    Znalezienie i udokumentowanie tego błędu zajęło 2 godziny. Prawdziwy atakujący wykorzystałby go w około 10 minut. Naprawa zajęła zespołowi deweloperskiemu około 4 godzin. Koszt braku testu: naruszenie danych podlegające zgłoszeniu RODO u ponad 50 klientów biznesowych.

    ← Powrót do Bloga