Skip to content
Powrót do bloga
Bezpieczeństwo

Bezpieczeństwo JWT: najlepsze praktyki, ataki i obrona

Zabezpiecz swoje JWT: powstrzymaj ataki alg:none i pomyłki algorytmów, ustal algorytmy, rotuj klucze, waliduj claims i bezpiecznie przechowuj tokeny.

13 min czytania

Bezpieczeństwo JWT: najlepsze praktyki, ataki i lista kontrolna na 2026

JSON Web Tokens napędzają większość współczesnych systemów uwierzytelniania, a mimo to praktyki, które utrzymują je w ryzach, są pomijane znacznie częściej, niż powinny. JWT to faktyczny standard formatu poświadczeń dla OAuth 2.0, OpenID Connect oraz wywołań między usługami w architekturze mikroserwisów. Co roku pojawia się przy nim kolejna porcja podatności CVE, a niemal wszystkie sprowadzają się do tych samych, łatwych do uniknięcia błędów: akceptowania niepodpisanych tokenów, zaufania algorytmowi wybranemu przez atakującego, użycia słabego sekretu podpisującego albo pominięcia walidacji claims.

Żeby JWT był bezpieczny, muszą zajść jednocześnie cztery rzeczy. Podpis pozostaje nienaruszony, atakujący nie może podmienić algorytmu, claims są faktycznie sprawdzane, a token przechowuje się w miejscu, z którego nie da się go łatwo ukraść. Wystarczy, że odpadnie jeden z tych warunków, a zamiast utwardzonego API masz obejście uwierzytelniania. Ten przewodnik omawia najpierw trzy ataki, które mają największe znaczenie, a potem obronę: wybór i ustalenie algorytmu, zarządzanie kluczami, walidację claims i przechowywanie tokenów. Na końcu znajdziesz listę kontrolną, którą możesz wkleić do przeglądu kodu.

Jak podpis JWT naprawdę cię chroni (i czego nie robi)

Zanim którykolwiek atak nabierze sensu, musisz przyswoić jeden fakt: JWT jest zakodowany, a nie zaszyfrowany. Podpisany token ma trzy segmenty Base64URL połączone kropkami: header.payload.signature. Nagłówek i payload to zwykły Base64URL z formatu JSON. Każdy, kto trzyma token w ręku, może odczytać dowolny zawarty w nim claim. Wklej jakikolwiek token do naszego dekodera JWT, a zobaczysz nagłówek i payload rozpisane w czytelnym JSON-ie, bez podawania klucza. Payload jest jawny z założenia.

Skąd więc bierze się bezpieczeństwo? Z podpisu i wyłącznie z podpisu. To wartość kryptograficzna obliczana z nagłówka i payloadu przy użyciu sekretu (HMAC) albo klucza prywatnego (RSA, ECDSA). Atakujący może swobodnie odczytać token, ale bez klucza podpisującego nie wyprodukuje innego tokenu, który przejdzie weryfikację. Na tej jednej własności wisi cały model zaufania.

Wynikają z tego dwie rzeczy. Po pierwsze, nigdy nie umieszczaj sekretów w payloadzie — haseł, kluczy API, pełnych danych osobowych — bo każdy, kto przechwyci token, je odczyta. Po drugie, całe twoje bezpieczeństwo opiera się na jednym kroku: poprawnej weryfikacji podpisu. I to właśnie ten krok biorą na cel atakujący. Jeśli chcesz dokładniejszego przewodnika po czytaniu tokenów segment po segmencie, zobacz jak zdekodować JWT.

3 krytyczne ataki na JWT (i jak powstrzymać każdy z nich)

Większość podatności JWT to wariacje na jeden temat: serwer ufa czemuś, co kontroluje atakujący. Oto trzy, które łamią uwierzytelnianie wprost — z mechanizmem każdego z nich i poprawką.

1. Atak alg:none — obejście przez niepodpisany token

Specyfikacja JWS dopuszcza wartość alg równą none, czyli „niepodpisany”. Token z alg:none ma pusty segment podpisu i mimo to kończy się kropką, jak header.payload.. Atak jest prosty: bierzesz poprawny token, zmieniasz alg w nagłówku na none, podstawiasz claims, jakie chcesz (powiedzmy "role": "admin"), i usuwasz podpis. Wczesne biblioteki JWT akceptowały to domyślnie, więc sfałszowany token bez przeszkód przechodził weryfikację. Bez klucza, bez podpisywania, a podszycie pełne.

Jak taki token wygląda, zobaczysz, wczytując przykład „alg:none” w naszym dekoderze JWT — pokazuje on wyraźne czerwone ostrzeżenie, że token jest niepodpisany i nigdy nie wolno go akceptować przy uwierzytelnianiu. Złożenie takiego tokenu samemu to minutowe ćwiczenie, po którym zagrożenie staje się namacalne.

Obroną jest jawna lista dozwolonych algorytmów przy każdym wywołaniu weryfikacji. Nie oddawaj tej decyzji domyślnemu ustawieniu biblioteki, bo starsze domyślne ustawienia bywały pobłażliwe, a jawne wskazanie kosztuje cię jedną dodatkową opcję.

// WRONG — the library may accept alg:none or any algorithm
jwt.verify(token, key);

// RIGHT — pin the exact algorithm you expect
jwt.verify(token, key, { algorithms: ['RS256'] });

none nigdy nie powinno pojawić się w tej tablicy. A jeśli twoja biblioteka nie pozwala ustalić algorytmów, wymień ją.

2. Pomyłka algorytmów — RS256 zdegradowane do HS256

To w praktyce najgroźniejsza podatność JWT, znana od 2015 roku i wciąż wyłapywana w audytach. Bierze na cel serwery, które decydują jak weryfikować, na podstawie pola alg w nagłówku — czyli tej jednej części tokenu, którą atakujący może przepisać.

Mechanizm wygląda tak. Twój serwer wystawia tokeny RS256: podpisuje kluczem prywatnym RSA i weryfikuje pasującym kluczem publicznym. Ten klucz publiczny z definicji jest publiczny — może leżeć w twoim endpointcie JWKS albo w repozytorium. Atakujący go bierze, zmienia nagłówek tokenu z RS256 na HS256 i podpisuje sfałszowany payload za pomocą HMAC-SHA256, używając łańcucha klucza publicznego jako sekretu HMAC. Teraz strona weryfikacji: jeśli twój kod odczytuje alg z nagłówka i pod nie dobiera HMAC, liczy HMAC-SHA256 na tokenie z tym samym kluczem publicznym w roli sekretu. Podpisy się zgadzają. Sfałszowany token przechodzi.

U podstaw leży zderzenie dwóch faktów: weryfikator zaufał nagłówkowi alg kontrolowanemu przez atakującego, a klucz publiczny RSA był dla atakującego dostępny, więc mógł go użyć jako klucza HMAC. Żaden z tych faktów osobno nie jest błędem. Klucz publiczny ma być publiczny, a nagłówek alg ma opisywać token. Dziura otwiera się dopiero wtedy, gdy twoja logika weryfikacji pozwala temu nagłówkowi wybrać typ klucza i algorytm, bo wtedy wartość zapisana przez atakującego steruje ścieżką kryptograficzną serwera.

// WRONG — verification method follows the header's alg field
jwt.verify(token, publicKeyOrSecret);

// RIGHT — hard-code the expected algorithm; never let the header choose
jwt.verify(token, publicKey, { algorithms: ['RS256'] });

Ustal algorytm asymetryczny jawnie (tylko RS256 lub ES256), trzymaj weryfikację HMAC na zupełnie osobnej ścieżce kodu niż weryfikację RSA i używaj wciąż utrzymywanej biblioteki, która rozróżnia typy kluczy. Nasz dekoder JWT oznacza każdy token z rodziny HS ostrzeżeniem o pomyłce z kluczem publicznym właśnie dlatego, że ten atak jest tak częsty: gdy token, którego spodziewałeś się jako asymetrycznego, przychodzi jako HS256, to ostrzeżenie jest dla ciebie sygnałem.

3. Słaby sekret HMAC — ataki siłowe i słownikowe

Gdy faktycznie używasz HMAC (HS256/384/512), całe bezpieczeństwo tokenu wisi na entropii jednego sekretu. Jeśli ten sekret jest krótki, jest słowem ze słownika albo czymś w rodzaju secret czy password123, atakujący, który przechwyci pojedynczy poprawny token, złamie go offline. Narzędzia w rodzaju hashcata przelatują miliardy kandydatów na sekundę względem podpisu tokenu. Gdy sekret padnie, atakujący wybije dowolny token, jaki zechce — ważne poświadczenia admin na zawsze.

Po cichu groźne jest tu to, że całość odbywa się offline. Atakujący nie wali w twój endpoint logowania, więc nie ma limitu częstotliwości do przekroczenia ani niczego w logach do wychwycenia. Przechwytuje jeden token, łamie sekret na własnym sprzęcie i wraca dopiero wtedy, gdy potrafi podpisywać tokeny przechodzące każdą kontrolę, jaką masz. Poprawka jest twarda: użyj co najmniej 32 losowych bajtów (256 bitów) z kryptograficznie bezpiecznego źródła i trzymaj sekret w menedżerze sekretów, nigdy w kodzie ani w repozytorium.

// WRONG — guessable, low entropy, crackable in seconds
const secret = "password123";

// RIGHT — 256 bits from a CSPRNG, then load from KMS at runtime
const secret = require('crypto').randomBytes(32).toString('base64');

Potrzebujesz szybko mocnej wartości? Nasz generator haseł tworzy łańcuchy o wysokiej entropii, które nadają się na klucz HMAC. Chcesz poczuć różnicę w praktyce? Podpisz testowy token mocnym sekretem w naszym koderze JWT — działa w całości w przeglądarce, więc sekret nigdy nie opuszcza twojej maszyny. A gdy weryfikacja przekracza granicę zaufania (wiele usług, weryfikatory zewnętrzne), odpuść HS256 i przejdź na algorytm asymetryczny, o którym za chwilę.

Wybór i ustalanie właściwego algorytmu

Na wyborze algorytmu wygrywa się albo przegrywa atak na pomyłkę, więc wybieraj z rozmysłem. Trzy, których faktycznie użyjesz:

AlgorytmTypKlucz podpisujący / weryfikującyKiedy stosować
HS256Symetryczny (HMAC)Jeden współdzielony sekretJedna granica zaufania, ta sama strona podpisuje i weryfikuje
RS256Asymetryczny (RSA)Klucz prywatny podpisuje / klucz publiczny weryfikujeMiędzy usługami, weryfikacja zewnętrzna, rotacja JWKS
ES256Asymetryczny (ECDSA)Klucz prywatny podpisuje / klucz publiczny weryfikujeTak samo jak RS256, mniejsze i szybsze klucze — preferowany dla nowych systemów

Reguła jest krótka. Jeśli ta sama strona podpisuje i weryfikuje w obrębie jednej granicy zaufania, HS256 jest w porządku i szybki. Jeśli weryfikować musi ktoś inny niż podpisujący — inna usługa, partner, klient publiczny — użyj algorytmu asymetrycznego i sięgnij po ES256: jego klucze i podpisy są znacznie mniejsze niż w RSA przy tej samej sile. Przykładowe tokeny HS256, RS256 i ES256 możesz podpisać obok siebie w koderze JWT i porównać ich strukturę oraz długość podpisu.

Cokolwiek wybierzesz, naprawdę liczy się ta sama obrona: ustal jeden jawny zestaw algorytmów przy wywołaniu weryfikacji i nigdy nie ufaj polu alg w nagłówku. Na liście dozwolonych algorytmów trzyma się reszta.

Zarządzanie kluczami i rotacja

Algorytmy są tylko tak bezpieczne, jak klucze pod nimi, a na obsłudze kluczy większość przewodników milknie. Przy HS256 sekret ma co najmniej 32 losowe bajty i żyje w menedżerze sekretów — AWS Secrets Manager, HashiCorp Vault albo Azure Key Vault. Przy algorytmach asymetrycznych klucz prywatny należy do HSM lub KMS i nigdy nie dotyka kodu aplikacji; klucz publiczny się publikuje, zwykle przez endpoint JWKS, który pobierają weryfikatory.

Rotacja ma być rutyną, a nie ratunkiem na pożar. Oznacz każdy klucz identyfikatorem kid (key ID) w nagłówku JWT, żeby weryfikatory wiedziały, który klucz podpisał dany token. Po stronie weryfikującej trzymaj niewielki zestaw ważnych kluczy — bieżący plus niedawny poprzedni — aby tokeny podpisane tuż przed rotacją weryfikowały się przez resztę swojego życia. Dzięki temu okresowi nakładania się rotacja przebiega gładko, zamiast kończyć się przestojem.

Krótka lista kontrolna dla kluczy:

  • Rotuj klucze podpisujące co najmniej co 90 dni, a natychmiast przy jakimkolwiek podejrzeniu kompromitacji.
  • Publikuj klucze publiczne przez JWKS; wersjonuj je za pomocą kid.
  • Trzymaj klucze prywatne i sekrety HMAC w KMS lub HSM — nigdy w gicie, nigdy w kodzie klienta, nigdy na sztywno w kodzie.
  • Po wycieku natychmiast zrotuj klucz i unieważnij wszystkie aktywne tokeny odświeżające.

Walidacja claims, której nie możesz pominąć

Sprawdzenie podpisu dowodzi, że token jest autentyczny. Nie dowodzi, że jest dla ciebie, teraz. Tym zajmuje się walidacja claims i jest to najtańsza obrona, jaką możesz dorzucić. Pięć claims trzeba sprawdzać przy każdym żądaniu:

  • exp (expiration) — odrzucaj tokeny, których wygaśnięcie jest w przeszłości.
  • nbf (not before) — odrzucaj tokeny użyte przed otwarciem ich okna ważności.
  • iat (issued at) — opcjonalnie odrzucaj tokeny, które są nieprawdopodobnie stare.
  • iss (issuer) — potwierdź, że token pochodzi od wystawcy, któremu ufasz.
  • aud (audience) — potwierdź, że token wybito dla twojej usługi. Brak kontroli aud to najczęstsza cicha luka: pozwala odtworzyć token wystawiony dla jednego API względem innego.

Większość bibliotek zwaliduje je za ciebie, gdy podasz oczekiwane wartości:

jwt.verify(token, key, {
  algorithms: ['ES256'],
  issuer: 'https://auth.example.com',
  audience: 'api.example.com',
  clockTolerance: 5, // seconds, for distributed clock skew
});

Dopuść niewielką tolerancję zegara — pięć sekund to typowa wartość — żeby drobne rozjazdy między serwerami nie odrzucały skądinąd ważnych tokenów. Nie daj się skusić, by ją podbijać; hojna tolerancja poszerza okno, w którym wygasły token wciąż działa, a to przecież okno, które exp ma zamykać. Żeby na oko sprawdzić wartości exp i iat w tokenie, wrzuć go do dekodera JWT i przelicz znaczniki czasu naszym konwerterem unix timestamp.

Czas życia tokenu i gdzie przechowywać JWT

Kontrole po stronie serwera to tylko połowa historii. To, gdzie klient trzyma token, decyduje, jak łatwo da się go ukraść — i to właśnie na przechowywaniu spotykają się XSS oraz przejęcie sesji. Wzorzec, który się sprawdza: krótko żyjący token dostępowy (15 do 60 minut) w parze z osobnym, dłużej żyjącym tokenem odświeżającym, który da się unieważnić.

Decyzja o przechowywaniu sprowadza się do jednego kompromisu:

Lokalizacja przechowywaniaNarażenie na XSSRyzyko CSRFRekomendacja
localStorageWysokie — dowolny JavaScript na stronie może go odczytaćBrakUnikaj dla tokenów sesji
Ciasteczko HttpOnly + Secure + SameSite=StrictNiskie — niewidoczne dla JavaScriptuWymaga ochrony przed CSRFZalecane dla sesji

Token w localStorage odczyta dowolny skrypt działający na stronie, więc pojedynczy błąd XSS ujawnia całą sesję, a atakujący odtwarza ją z własnej maszyny tak długo, jak token żyje. Ciasteczka HttpOnly JavaScript nie odczyta w ogóle, co zawęża szkody z XSS do tego, co atakujący zrobi wewnątrz żywej strony — źle, ale nie wyniesie stąd ukradzionego poświadczenia. Cena podejścia z ciasteczkiem jest taka, że teraz potrzebujesz ochrony przed CSRF, bo ciasteczka jadą automatycznie przy każdym żądaniu; SameSite=Strict plus token CSRF to załatwia. Token dostępowy trzymaj krótko, żeby wyciek miał mały promień rażenia, a token odświeżający umieść w ciasteczku HttpOnly, Secure, SameSite. Przy wylogowaniu lub podejrzeniu kompromitacji unieważnij token odświeżający po stronie serwera i zrotuj klucz podpisujący. Po szerszy kontekst wokół XSS, CSRF i bezpiecznych ciasteczek zajrzyj do naszego przewodnika najlepsze praktyki bezpieczeństwa webowego.

Lista kontrolna bezpieczeństwa JWT

Przejdź przez nią, zanim wdrożysz jakiekolwiek uwierzytelnianie oparte na JWT:

  • Weryfikacja ustala jawną listę dozwolonych algorytmów i odrzuca alg:none.
  • Weryfikacja asymetryczna ma na sztywno zakodowany oczekiwany algorytm i nigdy nie odczytuje alg z nagłówka (blokuje pomyłkę).
  • Sekrety HS256 mają co najmniej 32 losowe bajty i są ładowane z KMS.
  • Klucze prywatne żyją w HSM/KMS; klucze publiczne są publikowane przez JWKS i wersjonowane za pomocą kid.
  • Klucze podpisujące rotują co najmniej co 90 dni.
  • Każde żądanie waliduje exp, nbf, iat, iss i aud, z tolerancją zegara 5 sekund lub mniej.
  • Tokeny dostępowe żyją 15 do 60 minut; tokeny odświeżające żyją w ciasteczku HttpOnly.
  • Żadnych sekretów w payloadzie — jest zakodowany, a nie zaszyfrowany.

FAQ

Czy JWT jest bezpieczny domyślnie?

Nie. Bezpieczeństwo JWT zależy od konfiguracji. Musisz ustalić algorytm, odrzucić alg:none, użyć sekretu lub klucza o wysokiej entropii i walidować claims. Domyślne albo pobłażliwe konfiguracje bibliotek często otwierają drogę do obejścia uwierzytelniania.

Jaka jest najgroźniejsza podatność JWT?

Pomyłka algorytmów, w której RS256 zostaje zdegradowane do HS256, a klucz publiczny posłuży za sekret HMAC. Znana od 2015 roku, a mimo to wciąż wraca w audytach, bo bierze na cel serwery wybierające metodę weryfikacji na podstawie pola alg w nagłówku.

Czy powinienem używać HS256 czy RS256?

Używaj HS256, gdy ta sama strona podpisuje i weryfikuje w obrębie jednej granicy zaufania. Używaj RS256 lub ES256, gdy weryfikować musi inna usługa albo strona trzecia, lub gdy potrzebujesz rotacji JWKS. W nowych systemach sięgaj po ES256: mniejsze, szybsze klucze przy tej samej sile.

Gdzie powinienem przechowywać JWT?

Dla tokenów sesji sięgaj po ciasteczko HttpOnly, Secure, SameSite, bo JavaScript go nie odczyta, a pojedynczy błąd XSS go nie ukradnie. Tokenów sesji nie trzymaj w localStorage — dowolny XSS ujawnia tam całą sesję do odtworzenia.

Jak często powinienem rotować klucze podpisujące JWT?

Rutynowo co najmniej co 90 dni, a natychmiast przy jakimkolwiek podejrzeniu kompromitacji. Wersjonuj klucze za pomocą kid i trzymaj na weryfikatorze zarówno aktywny klucz, jak i niedawny poprzedni, żeby tokeny podpisane tuż przed rotacją nadal się walidowały.

Czy JWT można zmanipulować?

Nie bez klucza podpisującego — żaden atakujący nie sfałszuje tokenu, który przejdzie weryfikację. Ale jeśli twój serwer akceptuje alg:none, jest podatny na pomyłkę algorytmów albo używa słabego sekretu, podpis da się obejść. To błędy konfiguracji, a nie wady samego JWT.

Które claims muszę walidować?

Waliduj exp (expiration), nbf (not before), iat (issued at), iss (issuer) i aud (audience). Brak kontroli aud to najczęstsza cicha podatność: pozwala odtworzyć token przeznaczony dla jednej usługi względem innej.

Podsumowanie

Bezpieczeństwo JWT nie jest skomplikowane, ale każda warstwa musi się utrzymać. Podpis to twoja jedyna gwarancja, więc weryfikuj go poprawnie. Ustal jeden jawny algorytm i nigdy nie ufaj polu alg z nagłówka. Używaj mocnych, rotowanych kluczy trzymanych w KMS. Waliduj exp, nbf, iat, iss i aud przy każdym żądaniu. Trzymaj tokeny tam, gdzie XSS nie sięgnie.

Żeby przełożyć to na praktykę, wklej dowolny token do naszego dekodera JWT, sprawdź jego algorytm i claims oraz wychwyć ryzyka alg:none czy pomyłki z rodziny HS, a kodera JWT użyj, żeby poeksperymentować z podpisywaniem w całości w przeglądarce — klucze nigdy nie opuszczają urządzenia.

Tagi: jwt security authentication oauth api-security

Powiązane artykuły

Zobacz wszystkie artykuły