Bezpieczeństwo JWT: podatności, ataki i dobre praktyki

·

Tokeny JWT to dziś domyślny sposób na bezstanowe uwierzytelnianie w aplikacjach webowych. Ich elastyczność ma jednak drugą stronę: błędna konfiguracja zdarza się tu wyjątkowo często. Pentester nie patrzy na JWT jak na identyfikator sesji – widzi w nim kryptograficzną powierzchnię ataku.

Najważniejsze w skrócie

  • JWT jest zakodowany, a nie zaszyfrowany – kto ma token, ten odczyta każdy claim w payloadzie.
  • Algorithm confusion (podmiana RS256 na HS256) i algorytm none to wciąż najgroźniejsze błędy weryfikacji podpisu, jakie znajdujemy we własnych i starszych implementacjach.
  • Sekret HS256 to hasło. Przechwycony token łamie się offline Hashcatem, bez ani jednego kolejnego zapytania do aplikacji.
  • Parametry nagłówka podawane przez klienta – kid i jku – zamieniają weryfikację podpisu w powierzchnię wstrzyknięć.
  • Poprawny podpis nie mówi nic o ważności claimówexp, iss i aud trzeba egzekwować przy każdym żądaniu.
  • Access token zostaje w pamięci aplikacji, refresh token w ciasteczku HttpOnly, Secure, SameSite – nigdy w localStorage.

Budowa JWT: nagłówek, payload i podpis

JWT składa się z trzech części – nagłówka, payloadu i podpisu – rozdzielonych kropką (.). Przechwycony w proxy wygląda jak długi ciąg pozornie przypadkowych znaków. Przykład z życia:

eyJraWQiOiI5MTM2ZGRiMy1jYjBhLTRhMTktYTA3ZS1lYWRmNWE0NGM4YjUiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTY0ODAzNzE2NCwibmFtZSI6IkNhcmxvcyBNb250b3lhIiwic3ViIjoiY2FybG9zIiwicm9sZSI6ImJsb2dfYXV0aG9yIiwiZW1haWwiOiJjYXJsb3NAY2FybG9zLW1vbnRveWEubmV0IiwiaWF0IjoxNTE2MjM5MDIyfQ.SYZBPIBg2CRjXAJ8vCER0LA_ENjII1JakvNQoP-Hw6GG1zfl4JyngsZReIfqRvIAEi5L4HV0q7_9qGhQZvy9ZdxEJbwTxRs_6Lb-fZTDpW6lKYNdMyjw45_alSCZ1fypsMWz_2mTpQzil0lOtps5Ei_z7mM7M8gCwe_AGpI53JxduQOaB5HkT5gVrv9cKu9CsW5MS6ZbqYXpGyOG5ehoxqm8DL5tFYaW3lB50ELxi0KsuTKEbD0t5BCl0aCR2MBJWAbN-xeLwEenaqBiwPVvKixYleeDQiBEIylFdNNIMviKRgXiYuAvMziVPbwSgkZVHeEdF5MQP1Oe2Spac-6IfA

Nagłówek i payload to zwykłe obiekty JSON zakodowane w Base64Url. Zakodowane, nie zaszyfrowane – każdy, kto przechwyci token, odczyta ich zawartość bez żadnego klucza.

Wizualizacja budowy JWT

JWT – formalnie JWS, czyli JSON Web Token w wersji podpisanej – ma trzy sekcje rozdzielone kropkami: Header.Payload.Signature.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbmRyZWoiLCJyb2xlIjoiaGFja2VyIn0.MmM0ZGMxM2Y1NWQ2YTY2YTE
Nagłówek · algorytm i typ tokenu
{ "alg": "HS256", "typ": "JWT" }
Payload · dane
{ "sub": "andrej", "role": "hacker" }
Podpis
HMACSHA256(
    base64UrlEncode(header) + "." +
    base64UrlEncode(payload),
    "secret"
) = MmM0ZGMxM2Y1NWQ2YTY2YTE

Nagłówek

W nagłówku znajdziemy metadane samego tokenu: typ oraz algorytm kryptograficzny, którym go zabezpieczono. Pierwsza część powyższego tokenu dekoduje się do:

{
    "kid": "9136ddb3-cb0a-4a19-a07e-eadf5a44c8b5",
    "alg": "RS256"
}

Payload

Payload niesie właściwe claimy (deklaracje) o użytkowniku i to on jest głównym celem manipulacji podczas testu. Środkowa sekcja przykładu wygląda tak:

{
    "iss": "portswigger",
    "exp": 1648037164,
    "name": "Andrej Šebeň",
    "sub": "andrej",
    "role": "pentester",
    "email": "andrej@haxoris.com",
    "iat": 1516239022
}

Podpis

Skoro nagłówek i payload może odczytać oraz zmienić każdy, całe bezpieczeństwo mechanizmu opartego na JWT trzyma się na podpisie kryptograficznym.

Serwer wystawiający token liczy skrót z nagłówka i payloadu zakodowanych w Base64Url, używając tajnego klucza podpisującego. Daje to dwie gwarancje:

  1. Podpis wynika bezpośrednio z reszty tokenu, więc zmiana jednego bajtu w nagłówku lub payloadzie rozjeżdża go z treścią.
  2. Bez znajomości tajnego klucza serwera nie powinno dać się wygenerować poprawnego podpisu dla podrobionego nagłówka czy payloadu.

Czego atakujący szuka w JWT

JWT jest czytelny z założenia, więc dla atakującego to materiał rekonesansowy, a nie zaszyfrowana paczka. Błędy logiczne siedzą zwykle w payloadzie. Te claimy sprawdzamy od razu:

ClaimDo czego służyCo sprawdzamy
iss (Issuer)Wskazuje, kto wystawił token.Czy backend faktycznie to weryfikuje? System, który przyjmuje tokeny od nieuprawnionego wystawcy, jest podatny na token confusion.
sub (Subject)Użytkownik lub podmiot, który token reprezentuje – najczęściej identyfikator konta.Insecure Direct Object Reference (IDOR). Czy da się podmienić wartość na admin albo user_id=1 i obejść autoryzację?
aud (Audience)Wskazuje zamierzonego odbiorcę.Czy token wystawiony dla jednego mikroserwisu zadziała w innym, dużo bardziej uprzywilejowanym?
exp (Expiration Time)Uniksowy znacznik czasu, po którym token wygasa.Czy serwer to egzekwuje? Regularnie trafiamy na API, które w nieskończoność przyjmują wygasłe tokeny.
nbf (Not Before)Moment, przed którym tokenu nie wolno zaakceptować.Bardzo często w ogóle nie trafia do logiki walidacji.
iat (Issued At)Kiedy token powstał.Przydaje się do policzenia czasu życia tokenów i wyłapania tych, które nigdy nie są rotowane.

Rekonesans payloadu: kopalnia wycieków informacji

Payload widzi każdy posiadacz tokenu, więc deweloper powinien traktować go jak treść publiczną. W testach regularnie znajdujemy w niestandardowych claimach szczegóły, których nie powinno tam być:

  • Mapa sieci wewnętrznej: claimy w rodzaju "node_ip": "10.0.4.12", "cluster": "prod-us-east-1a" czy "db_host": "internal-db.local" zdradzają architekturę sieci i używane zakresy adresów.
  • Wyciek frameworka i dostawcy tożsamości: konwencja nazewnicza claimów odsłania stos technologiczny. Claim http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name mówi wprost o backendzie Microsoft .NET / Entra ID, więc możemy od razu celować w podatności typowe dla tego stosu.
  • Dane osobowe: niepotrzebnie wystawione adresy e-mail, numery telefonów, imiona i nazwiska czy adresy zamieszkania – a razem z nimi gotowe naruszenie przepisów RODO i CCPA.
  • Enumeracja tenantów i ról: claimy "tenant_id": "uuid", "is_god_mode": false albo "permissions": ["read:profile"] odsłaniają schemat autoryzacji i podają nam gotowe nazwy parametrów do eskalacji uprawnień.

Przykład nadmiernie gadatliwego payloadu JWT, przechwyconego w praktyce:

{
  "iss": "auth.internal-corp.local",
  "sub": "usr_99812",
  "email": "andrej@haxoris.com",
  "role": "admin",
  "internal_ip": "10.240.12.88",
  "db_shard": "shard-04-eu",
  "is_admin": true,
  "exp": 1748037164
}

Bearer token a refresh token: architektura i przechowywanie

Bezpieczna implementacja JWT opiera się na dwóch tokenach: krótko żyjącym access tokenie (bearer token) i długo żyjącym refresh tokenie. Błędne miejsce ich przechowywania to najczęstsza podatność, jaką w tym obszarze raportujemy.

Access token (bearer token)

  • Access token autoryzuje żądania do API i celowo żyje krótko – zwykle od 5 do 15 minut.
  • Jego miejsce to wyłącznie pamięć aplikacji. localStorage i sessionStorage odpadają: jedna udana podatność XSS wystarczy, żeby token trafił do atakującego.

Refresh token

  • Refresh token służy tylko do pobrania nowego access tokenu po wygaśnięciu poprzedniego.
  • Jego miejsce to ciasteczko HttpOnly, Secure, SameSite: JavaScript go nie odczyta, kradzież przez XSS robi się dużo trudniejsza, a przy okazji spada ryzyko CSRF.

Warsztat pentestera: typowe ataki na JWT

Testując aplikację webową, przechodzimy zwykle przez poniższe wektory ataku.

A. Błędy weryfikacji podpisu

Wiele podatności JWT bierze się nie ze złamanej kryptografii, tylko z niepoprawnej weryfikacji podpisu. Część bibliotek akceptowała kiedyś algorytm none, co pozwalało usunąć podpis w całości i dowolnie zmieniać claimy – choćby rolę użytkownika. Dziś liczące się biblioteki domyślnie odrzucają none, ale w starszych aplikacjach i we własnych implementacjach błąd wciąż wychodzi. Przy niestandardowym stosie warto go sprawdzić.

Drugi klasyk to algorithm confusion. Jedna z najgroźniejszych podatności JWT, więc warto prześledzić ją krok po kroku.

  • RS256 to algorytm asymetryczny. Serwer podpisuje tokeny kluczem prywatnym, a weryfikuje odpowiadającym mu kluczem publicznym. Klucz publiczny bywa przy tym dostępny bez żadnych przeszkód – choćby pod endpointem /.well-known/jwks.json.
  • HS256 to algorytm symetryczny. Serwer podpisuje i weryfikuje jednym współdzielonym sekretem.

Atak wykorzystuje sytuację, w której serwer przyjmuje oba algorytmy i nigdzie nie zapisał na sztywno, którego oczekuje:

  1. Atakujący pobiera klucz publiczny serwera – zwykle wprost z endpointu JWKS.
  2. Podmienia payload, na przykład "role": "user" na "role": "admin".
  3. W nagłówku zmienia "alg": "RS256" na "alg": "HS256".
  4. Podpisuje cały zmieniony token (nagłówek i payload) jeszcze raz, używając klucza publicznego jako sekretu HS256.
  5. Serwer widzi alg: HS256 i weryfikuje podpis kluczem publicznym w roli sekretu HMAC. Zgadza się – podrobiony token przechodzi.
Atak algorithm confusion na JWT: zmiana nagłówka alg z RS256 na HS256 i ponowne podpisanie tokenu kluczem publicznym serwera w roli sekretu HMAC

Źródłem problemu nie jest złamana kryptografia, tylko ślepe zaufanie do nagłówka alg przysłanego przez klienta – to on decyduje, jak token zostanie zweryfikowany. Normalnie podmiana alg unieważniłaby podpis, bo podpis obejmuje nagłówek razem z payloadem. Tyle że atakujący podpisuje cały zmieniony token od nowa, więc dla podmienionego algorytmu podpis jest w pełni poprawny.

Nowoczesne biblioteki w dużej mierze zamknęły tę furtkę, ale w starszych aplikacjach i we własnych implementacjach JWT nadal ją spotykamy.

B. Słabe sekrety podpisujące

Aplikacja z algorytmem symetrycznym, na przykład HS256, jest dokładnie tak bezpieczna, jak sekret użyty do podpisu.

Przechwycony w teście token łamiemy całkowicie offline, narzędziami takimi jak Hashcat czy John the Ripper. Nazwa firmy, słowo ze słownika albo przewidywalna fraza padają zwykle w kilka minut, a wtedy można wystawiać poprawne tokeny dla dowolnego użytkownika – bez ani jednego kolejnego zapytania do aplikacji.

C. Wstrzyknięcia w parametry nagłówka

Nagłówek JWT może zawierać opcjonalne parametry kid (Key ID) i jku (JWK Set URL), które podpowiadają serwerowi, którego klucza użyć przy weryfikacji. Wartości pochodzą od klienta, więc zaufanie im bez walidacji otwiera poważne dziury.

Typowe wektory ataku:

  • podmiana parametru jku na endpoint JWKS kontrolowany przez atakującego;
  • directory traversal przez kid, jeśli klucze wczytywane są wprost z systemu plików;
  • SQL injection tam, gdzie wartość kid trafia do zapytania przez konkatenację.

Dojrzałe biblioteki radzą sobie z tymi scenariuszami, ale własna logika weryfikacji wciąż dostarcza nam podatności do raportu.

D. Błędy walidacji claimów

Poprawny podpis nie oznacza jeszcze, że tokenowi można ufać. Aplikacja musi sprawdzić każdy claim istotny dla bezpieczeństwa.

W pentestach regularnie trafiamy na API, które:

  • ignorują exp i przyjmują wygasłe tokeny;
  • nie sprawdzają odbiorcy (aud), więc ten sam token zadziała w zupełnie innej usłudze;
  • ufają dowolnej wartości wystawcy (iss);
  • opierają się wyłącznie na claimach kontrolowanych przez klienta – role, tenant, permissions – i nie egzekwują autoryzacji po stronie serwera.

W praktyce takie błędy logiczne często ważą więcej niż słabości kryptograficzne, bo pozwalają wykorzystać całkowicie poprawny token w sposób, którego nikt nie przewidział.

Narzędzia do testowania bezpieczeństwa JWT

Ręczne dekodowanie Base64 i dłubanie przy podpisie to strata czasu. Do automatyzacji sięgamy po narzędzia:

  • JWTAuditor: platforma działająca po stronie klienta, zrobiona pod pentesterów. Szybki podgląd tokenu, automatyczne wykrywanie podatności (algorytm none, wstrzyknięcia w nagłówek) i łamanie offline – bez ryzyka, że wrażliwe tokeny klienta trafią do cudzej telemetrii.
  • Burp Suite (rozszerzenie JWT Editor): podstawa testów dynamicznych – modyfikacja tokenów w locie, na ruchu HTTP przechwyconym w proxy.
  • JWT.io: utrzymywany przez Auth0 standard do szybkiego dekodowania ręcznego oraz sprawdzenia struktury i podpisu na etapie rekonesansu.
  • JWTLens: lekka alternatywa nastawiona na prywatność. Dekoduje i weryfikuje tokeny w całości w przeglądarce, natywnym Web Crypto API, więc tokeny i klucze nie opuszczają komputera. Weryfikacja nie wysyła żadnego zapytania sieciowego, co czyni z niej bezpieczny sposób na sprawdzenie algorytmu i podpisu.

Jak się bronić: praktyczne zalecenia

Żeby aplikacja nie trafiła do sekcji podatności krytycznych w raporcie z testów, zespół inżynierski powinien wymusić kilka podstawowych zabezpieczeń:

  • Algorytm zapisany na sztywno: przychodzący token nigdy nie może decydować o sposobie własnej weryfikacji. Ustaw w konfiguracji walidacji jeden konkretny algorytm (na przykład RS256) i odrzucaj none bezwarunkowo.
  • Pełna walidacja claimów: poprawny podpis dowodzi tylko tego, że nikt nie ruszył treści – a nie tego, że token jest w tej chwili ważny dla danego użytkownika. Sprawdzaj exp (termin ważności), iss (wystawcę) i aud (odbiorcę) przy każdym pojedynczym żądaniu.
  • Sekret symetryczny to hasło roota: jeśli architektura wymaga podpisu symetrycznego (HS256), klucz ma mieć minimum 256 bitów entropii, pochodzić z kryptograficznie bezpiecznego generatora liczb losowych i leżeć w zmiennych środowiskowych albo w menedżerze sekretów – nigdy w kodzie źródłowym.
  • Kodowanie to nie szyfrowanie: w payloadzie JWT nie ma miejsca na dane wrażliwe, dane osobowe ani hasła. Kto przechwyci token, zdekoduje Base64 w sekundę.
  • Osobne miejsca przechowywania: krótko żyjące access tokeny nie mogą lądować w localStorage ani sessionStorage – jedna podatność XSS wystarczy, żeby je stamtąd wyprowadzić. Access token zostaje w pamięci przeglądarki, refresh token trafia do ciasteczka Secure, HttpOnly, SameSite.

Wyższy poziom: wzorzec phantom token

W środowiskach o podwyższonych wymaganiach najskuteczniejszą obroną przed kradzieżą po stronie klienta jest nieujawnianie JWT frontendowi w ogóle. Rozbudowane architektury korporacyjne często realizują to wzorcem „phantom token”:

  1. Serwer autoryzacji wydaje aplikacji klienckiej losowy, nieprzejrzysty ciąg sesyjny zamiast JWT.
  2. Klient wysyła ten ciąg przy każdym żądaniu do API.
  3. Brama API przechwytuje żądanie, weryfikuje ciąg w serwerze autoryzacji albo w magazynie sesji i wymienia go na pełny, podpisany JWT.
  4. Brama przekazuje właściwy JWT do wewnętrznych mikroserwisów.

Najczęstsze pytania o bezpieczeństwo JWT

01

Czy JWT jest zaszyfrowany?

Nie. Nagłówek i payload zwykłego podpisanego JWT (czyli JWS) są tylko zakodowane w Base64Url, a nie zaszyfrowane. Każdy, kto ma token, odczyta wszystkie zawarte w nim claimy. Podpis chroni integralność, nie poufność – dlatego w payloadzie JWT nie umieszcza się danych wrażliwych ani danych osobowych.

02

Na czym polega algorithm confusion w JWT?

Algorithm confusion pojawia się wtedy, gdy o sposobie weryfikacji podpisu decyduje nagłówek alg samego tokenu. Atakujący bierze publiczny klucz RSA serwera, zmienia nagłówek z RS256 na HS256, modyfikuje payload i podpisuje cały token od nowa, używając tego klucza publicznego jako sekretu HMAC. Serwer, który akceptuje oba algorytmy, uzna taki podpis za poprawny. Lekarstwem jest zapisanie oczekiwanego algorytmu na sztywno po stronie serwera.

03

Gdzie przechowywać access token i refresh token w przeglądarce?

Krótko żyjący access token zostaje wyłącznie w pamięci aplikacji. Długo żyjący refresh token trafia do ciasteczka HttpOnly, Secure, SameSite, żeby JavaScript nie mógł go odczytać. Żaden z nich nie może wylądować w localStorage ani sessionStorage – jedna podatność XSS wystarczy, żeby je stamtąd wyprowadzić.

04

Jak atakujący łamią sekrety podpisujące HS256?

Przechwycony token HS256 da się atakować całkowicie offline, narzędziami takimi jak Hashcat czy John the Ripper, bez żadnej dalszej interakcji z aplikacją. Słabe sekrety – nazwa firmy, słowo ze słownika, przewidywalna fraza – padają często w kilka minut, a potem atakujący wystawia poprawne tokeny dla dowolnego użytkownika. Sekret symetryczny potrzebuje minimum 256 bitów entropii z kryptograficznie bezpiecznego generatora.

05

Czy poprawny podpis wystarczy, żeby zaufać tokenowi JWT?

Nie. Poprawny podpis dowodzi wyłącznie tego, że nikt nie zmienił treści tokenu. Aplikacja i tak musi zweryfikować każdy claim istotny dla bezpieczeństwa: exp – termin ważności, iss – wystawcę, aud – zamierzonego odbiorcę. Nie wolno jej też polegać na claimach przysyłanych przez klienta, takich jak role czy permissions, bez egzekwowania autoryzacji po stronie serwera.

W czym pomaga Haxoris

Bezpieczeństwo tokenów rzadko łamie kryptografia – łamie je konfiguracja. Znajdziemy takie luki w Twojej aplikacji, zanim znajdzie je ktoś inny:

  • Testy penetracyjne aplikacji webowych i API, ze szczególnym naciskiem na uwierzytelnianie i autoryzację.
  • Przegląd logiki wystawiania i weryfikacji JWT oraz zarządzania kluczami, razem z obsługą JWKS i rotacją kluczy.
  • Ocena siły sekretów symetrycznych – łamanie offline.
  • Przegląd projektu: gdzie leżą tokeny, jak długo żyją sesje, jak zbudowana jest obsługa refresh tokenów.

Podsumowanie

JWT nie jest niebezpieczny z natury. Staje się niebezpieczny w chwili, gdy aplikacja pozwala tokenowi zdecydować o sposobie własnej walidacji albo myli poprawny podpis z autoryzacją. Algorytm na sztywno, walidacja każdego claimu, sekret traktowany jak hasło roota i tokeny poza magazynem przeglądarki – przy tych czterech nawykach większość podatności z tego artykułu zniknie z kolejnego raportu z testów penetracyjnych.

Źródła i polecane lektury

Metodyki i szczegóły techniczne opisane wyżej opierają się na następujących źródłach:

  1. PortSwigger Web Security Academy. JWT Security Vulnerabilities. Szczegółowe omówienie błędów weryfikacji podpisu i wstrzyknięć w nagłówek. portswigger.net/web-security/jwt
  2. InfoSec Writeups. JWT Pentesting: A Journey from Token to Takeover. Praktyczne studia przypadków wykorzystania błędnej konfiguracji tokenów. infosecwriteups.com
  3. Auth0 / JWT.io. Introduction to JSON Web Tokens. Podstawowa dokumentacja standardu: budowa tokenów i RFC 7519. jwt.io/introduction
  4. JWTAuditor. Security Testing Tool. Narzędzie do manipulacji payloadem i automatycznego wykrywania podatności. jwtauditor.com
Pavol Litauszki

Autor

Pavol Litauszki

Nie czekaj na atakujących – sprawdź, gdzie pęka uwierzytelnianie w Twojej aplikacji.

Umów bezpłatną konsultację