Przejdź do treści
INDEKS ANALIZ BREACHROAD / NOTA TECHNICZNA

Bezpieczeństwo JWT: kid, JWKS i rotacja kluczy

Jak działa weryfikacja JWT i gdzie się psuje: alg confusion, kid injection, JWKS, walidacja iss/aud, unieważnianie tokenów, testowanie i detekcja.

MATERIAŁ PUBLICZNY
AUTOR
/ CEO Breachroad · OSCP · PNPT
PUBLIKACJA
25 lipca 2026
CZAS CZYTANIA
18 min czytania
TEMAT
Tożsamość i dostęp
Bezpieczeństwo JWT: kid, JWKS i rotacja kluczy

JWT nie jest sesją. To jest zdanie, od którego warto zacząć każdą rozmowę o tokenach, bo z niego wynikają wszystkie pozostałe konsekwencje. Klasyczna sesja serwerowa trzyma stan po stronie aplikacji, a ciasteczko jest tylko nieprzewidywalnym wskaźnikiem do tego stanu. JWT odwraca układ: token niesie twierdzenia o użytkowniku, a serwer ufa im wyłącznie dlatego, że podpis się zgadza. Cały ciężar bezpieczeństwa przenosi się więc z losowości identyfikatora na poprawność weryfikacji kryptograficznej i walidacji pól.

To przesunięcie odpowiedzialności jest źródłem większości błędów, jakie widuje się w audytach. Aplikacja rzadko „nie sprawdza podpisu” wprost. Znacznie częściej sprawdza go w sposób, który atakujący potrafi wskazać na własny klucz, albo sprawdza podpis idealnie i pomija to, że token został wystawiony dla zupełnie innego odbiorcy.

Anatomia tokenu i granica zaufania

Token w formacie kompaktowym to trzy części rozdzielone kropkami: nagłówek, ładunek i podpis, każda zakodowana base64url. Kodowanie nie jest szyfrowaniem — nagłówek i ładunek czyta każdy, kto ma token. Jeżeli w tokenie ląduje numer PESEL, adres, rola wewnętrzna lub identyfikator kontrahenta, to znaczy, że te dane są jawne dla użytkownika, dla przeglądarki, dla każdego proxy po drodze i dla każdego, kto zajrzy do logów.

Nagłówek deklaruje algorytm (alg) oraz zwykle identyfikator klucza (kid). Ładunek zawiera twierdzenia: kto wystawił token (iss), dla kogo jest przeznaczony (aud), kogo dotyczy (sub), od kiedy i do kiedy jest ważny (nbf, exp), a często również unikalny identyfikator (jti). Podpis wiąże obie części z kluczem wystawcy.

Granica zaufania biegnie dokładnie w jednym miejscu: wszystko, co jest w tokenie, pochodzi od klienta, dopóki podpis nie zostanie zweryfikowany zaufanym kluczem. Nagłówek jest częścią tokenu, więc również pochodzi od klienta. To brzmi banalnie, a jest sednem dwóch najczęstszych klas podatności.

Klasa pierwsza: algorytm z tokenu

Najstarszy błąd polega na tym, że aplikacja czyta alg z nagłówka i na tej podstawie wybiera sposób weryfikacji. Konsekwencje są dwie i obie są krytyczne.

Pierwsza to wartość none. Specyfikacja przewiduje „niezabezpieczony JWS”, więc biblioteka, która potraktuje alg: none jako prawidłowy tryb, zaakceptuje token bez podpisu — a więc token dowolnej treści. Dojrzałe biblioteki blokują to domyślnie, ale własne implementacje weryfikacji i stare wersje zależności nadal się na tym wykładają.

Druga to pomieszanie rodzin algorytmów. Gdy serwer wystawia tokeny podpisane RS256, jego klucz publiczny jest z definicji jawny. Jeżeli weryfikator wybiera algorytm na podstawie nagłówka i dla HS256 użyje „klucza” z konfiguracji, którym jest właśnie ten klucz publiczny, to atakujący może podpisać własny token symetrycznie, używając publicznie dostępnej wartości jako sekretu. Podpis się zgodzi, bo obie strony liczą HMAC z tego samego materiału.

Naprawa jest krótka i nienegocjowalna: weryfikator ma znać algorytm z konfiguracji, nie z tokenu. Akceptujemy zamkniętą listę algorytmów przypisaną do konkretnego wystawcy, a token z innym alg odrzucamy przed jakąkolwiek próbą liczenia podpisu.

Klasa druga: kid jako wskaźnik sterowany przez klienta

Pole kid służy do wskazania, którym kluczem podpisano token — jest to konieczne, gdy wystawca ma więcej niż jeden klucz, a więc zawsze, gdy prowadzi rotację. Problem zaczyna się wtedy, gdy wartość z kid trafia do operacji, która potrafi zrobić coś więcej niż wyszukać wpis w mapie.

W praktyce widuje się trzy warianty. kid użyty jako fragment ścieżki do pliku z kluczem otwiera drogę do path traversal i wskazania pliku o przewidywalnej zawartości. kid wklejony do zapytania SQL zamienia weryfikację tokenu w wektor SQL injection wykonywany przed uwierzytelnieniem. kid użyty jako klucz w cache rozproszonym pozwala czasem podstawić wpis, jeżeli ten sam cache przyjmuje dane z innego źródła.

Osobną rodziną są nagłówki jku i x5u, czyli adresy URL wskazujące zestaw kluczy lub certyfikat. Jeżeli weryfikator pobierze klucz spod adresu podanego w tokenie, to atakujący dostarcza własny klucz razem z własnym tokenem i weryfikacja zawsze się powiedzie. Te nagłówki mają sens wyłącznie z twardą listą dozwolonych adresów, a w większości wdrożeń najlepszą decyzją jest ich całkowite odrzucanie.

Bezpieczny wzorzec jest prosty: kid to etykieta do wyszukania w zestawie kluczy pobranym z zaufanego źródła. Nie ścieżka, nie URL, nie parametr zapytania. Nieznane kid kończy się odrzuceniem tokenu, a nie pobraniem czegokolwiek z sieci.

Klasa trzecia: poprawny podpis, zły token

Ta kategoria jest podstępna, bo aplikacja robi kryptografię bezbłędnie i mimo to autoryzuje żądanie, którego nie powinna. Podpis mówi tylko tyle, że token wystawił ktoś, kogo klucz znamy. Nie mówi, że wystawił go dla nas, że token nadal obowiązuje ani że dotyczy tej aplikacji.

Walidacja pełna obejmuje kilka niezależnych sprawdzeń. iss musi odpowiadać dokładnie oczekiwanemu wystawcy — porównanie napisów, nie dopasowanie fragmentu, bo https://issuer.example.com.attacker.tld zawiera oczekiwany prefiks. aud musi zawierać identyfikator tej konkretnej usługi; jeżeli wspólny dostawca tożsamości wystawia tokeny dla dziesięciu mikroserwisów, brak tej kontroli oznacza, że token do usługi raportowej otwiera usługę płatności. exp i nbf sprawdzamy z małym marginesem na rozjazd zegarów, ale margines liczony w minutach zamienia wygaśnięcie w fikcję. Jeżeli token niesie scope, roles lub tenant, to każde z tych pól musi być egzekwowane po stronie zasobu, a nie tylko wyświetlane w interfejsie.

Warto też pamiętać o kierunku zaufania między systemami. Token wewnętrzny, wystawiony przez usługę A dla usługi B, nie powinien być akceptowany przez usługę C tylko dlatego, że wszystkie trzy używają tego samego klucza. To jest dokładnie ta sytuacja, w której aud przestaje być formalnością.

JWKS w praktyce: publikacja, cache i rotacja

Zestaw kluczy publicznych (JWKS) jest zwykle publikowany pod znanym adresem wystawcy i pobierany przez weryfikatory. Operacyjnie ważne są trzy rzeczy.

Po pierwsze, cache z sensownym czasem życia. Pobieranie zestawu przy każdym żądaniu zamienia dostawcę tożsamości w pojedynczy punkt awarii i zaproszenie do wysycenia limitów. Pobieranie raz na dobę powoduje, że rotacja klucza wywraca ruch na wiele godzin.

Po drugie, okno nakładania się kluczy. Poprawna rotacja wygląda tak: nowy klucz pojawia się w JWKS, ale jeszcze nie podpisuje; po czasie dłuższym niż cache weryfikatorów wystawca zaczyna nim podpisywać, zostawiając stary klucz w zestawie na czas życia najdłuższego wystawionego tokenu; dopiero potem stary klucz znika. Pominięcie któregokolwiek kroku daje klasyczną awarię „wszyscy dostają 401 po wdrożeniu”.

Po trzecie, kontrolowane odświeżanie na nieznany kid. Rozsądny weryfikator, widząc kid spoza cache, może odświeżyć zestaw — ale z limitem częstotliwości. Bez limitu wystarczy strumień tokenów z losowym kid, żeby zamienić weryfikację w generator ruchu do dostawcy tożsamości.

Do tego dochodzi higiena samego pobierania: tylko HTTPS, tylko adres z konfiguracji, walidacja odpowiedzi, twardy timeout i limit rozmiaru. Zestaw kluczy pobierany bez limitów to gotowy kanał na wysycenie pamięci.

Sekrety symetryczne i tokeny wewnętrzne

HS256 bywa wybierany dlatego, że jest prosty. Cena jest taka, że klucz do weryfikacji jest jednocześnie kluczem do wystawiania. Każda usługa, która sprawdza tokeny, potrafi też tworzyć dowolne. W architekturze z kilkunastoma mikroserwisami oznacza to, że kompromitacja najmniej istotnego komponentu daje możliwość wystawienia tokenu administratora.

Jeżeli tokeny przekraczają granicę zespołu, usługi lub strefy zaufania, właściwym wyborem jest algorytm asymetryczny. Sekrety symetryczne zostawiamy do zastosowań lokalnych i krótkich, z sekretem o odpowiedniej entropii, trzymanym w menedżerze sekretów, nie w repozytorium i nie w obrazie kontenera.

Czas życia i unieważnianie

Token podpisany jest ważny do wygaśnięcia i nic tego nie zmienia — to jest cena bezstanowości. Dlatego projektuje się krótkie tokeny dostępowe i osobny mechanizm odnawiania, który jest stanowy i podlega unieważnieniu.

Kiedy potrzebne jest natychmiastowe wylogowanie lub odcięcie skradzionego tokenu, potrzebna jest lista unieważnionych identyfikatorów jti albo znacznik „ważne od” per użytkownik, sprawdzany przy weryfikacji. To odbiera część korzyści z bezstanowości, ale jest jedynym uczciwym sposobem, żeby przycisk „wyloguj wszędzie” faktycznie działał.

Osobnym tematem jest wiązanie tokenu z klientem. Token na okaziciela działa u każdego, kto go zdobędzie. Mechanizmy takie jak DPoP lub tokeny wiązane z certyfikatem klienta sprawiają, że sam token bez klucza posiadacza jest bezużyteczny. Warto je rozważyć wszędzie tam, gdzie skutki kradzieży tokenu są nieproporcjonalnie duże w stosunku do kosztu wdrożenia.

Gdzie token żyje i gdzie wycieka

Najczęstsze wycieki nie są kryptograficzne. Token w parametrze zapytania trafia do logów serwera, historii przeglądarki, nagłówka Referer i systemów analitycznych. Token w localStorage jest dostępny dla każdego skryptu na stronie, więc każdy XSS staje się przejęciem sesji. Token w ciasteczku bez atrybutów HttpOnly, Secure i SameSite dziedziczy wszystkie problemy ciasteczek.

Do tego dochodzą logi aplikacyjne i systemy obserwowalności. Nagłówek Authorization bywa logowany „na czas debugowania”, a potem zostaje na stałe. Redakcja tego nagłówka powinna być domyślną regułą w bibliotece logującej, nie decyzją pojedynczego programisty.

Metodyka testu

Test bezpieczeństwa JWT zaczyna się od przeglądu, nie od wysyłania żądań. Ustal, kto wystawia tokeny, jaka biblioteka je weryfikuje, w której wersji i z jakimi parametrami. Bardzo często cała odpowiedź leży w jednej linijce konfiguracji, w której brakuje listy dozwolonych algorytmów albo oczekiwanego odbiorcy.

Następnie przygotuj macierz przypadków negatywnych i sprawdzaj każdy wariant osobno, na własnym koncie testowym i w środowisku, w którym masz zgodę na testy: token po terminie, token sprzed okresu ważności, token z innym wystawcą, token z odbiorcą innej usługi, token podpisany kluczem, którego serwer nie zna, token z podmienionym algorytmem, token z nieznanym kid, token z obciętym podpisem. W każdym przypadku interesuje cię nie tylko kod odpowiedzi, ale też czas i treść komunikatu — różnice potrafią zdradzić, że weryfikacja zatrzymała się w innym miejscu, niż zakładałeś.

Trzeci krok to sprawdzenie, co się dzieje z twierdzeniami po weryfikacji. Token może być poprawny, a mimo to zawierać rolę, której użytkownik nie powinien mieć, jeżeli usługa wystawiająca pozwala wpłynąć na jej wartość podczas logowania lub rejestracji. Ta ścieżka bywa pomijana, bo formalnie nie jest błędem tokenu, tylko błędem wystawcy.

Testy negatywne warto zamknąć w zestawie automatycznym uruchamianym w CI. Weryfikacja tokenu to kod, który zmienia się rzadko i psuje cicho — najczęściej przy podbiciu wersji biblioteki albo zmianie dostawcy tożsamości.

Detekcja po stronie obrony

Logowanie powinno obejmować iss, aud, kid i algorytm z zaakceptowanych tokenów oraz powód odrzucenia dla odrzuconych. Na tej podstawie da się zbudować kilka bardzo tanich, a skutecznych reguł.

Pojawienie się algorytmu spoza listy dozwolonych to sygnał, że ktoś testuje weryfikator — legalny klient nigdy tego nie zrobi. Nieznane kid w rosnącej liczbie żądań wskazuje na próbę wymuszenia odświeżeń JWKS lub na zgadywanie ścieżek. Nagły wzrost odrzuceń z powodu aud po wdrożeniu zwykle oznacza błąd konfiguracji, ale bywa też pierwszym śladem tokenu przenoszonego między usługami. Wreszcie skok odrzuceń exp tuż po rotacji kluczy to znak, że okno nakładania było za krótkie.

Warto też monitorować czas życia tokenów w ruchu produkcyjnym. Jeżeli w środowisku pojawiają się tokeny ważne dłużej niż zakłada polityka, to znaczy, że jakiś komponent wystawia je poza standardową ścieżką.

Kiedy nie używać JWT

Bezstanowość ma sens, gdy naprawdę rozwiązuje problem: wiele usług, wiele stref, brak wspólnego magazynu sesji, wymóg weryfikacji offline. Jeżeli aplikacja jest jednym monolitem z jedną bazą, klasyczna sesja serwerowa jest prostsza i bezpieczniejsza: unieważnianie jest natychmiastowe, dane wrażliwe nie krążą po kliencie, a cała powierzchnia błędów opisana wyżej po prostu nie istnieje.

Wybór JWT powinien być decyzją architektoniczną z uzasadnieniem, a nie domyślnym ustawieniem szkieletu aplikacji.

Checklista

Weryfikator akceptuje wyłącznie algorytmy z listy w konfiguracji. Nagłówki jku i x5u są ignorowane lub ograniczone twardą listą adresów. kid służy tylko do wyszukania klucza w pobranym zestawie i nigdy nie trafia do ścieżki, zapytania ani innego interpretera. iss i aud są porównywane dokładnie i wymagane. exp i nbf są egzekwowane z marginesem liczonym w sekundach. Klucze mają zaplanowaną rotację z oknem nakładania dłuższym niż cache weryfikatorów. Odświeżanie JWKS ma limit częstotliwości, timeout i limit rozmiaru. Tokeny dostępowe są krótkie, a odnawianie jest stanowe i odwoływalne. Nagłówek Authorization jest redagowany w logach. Token nigdy nie podróżuje w parametrze zapytania. Testy negatywne weryfikacji działają w CI.

Wniosek

JWT jest dobrym narzędziem do przenoszenia twierdzeń między usługami i złym zamiennikiem sesji tam, gdzie sesja wystarcza. Bezpieczeństwo tokenu nie kończy się na tym, że podpis się zgadza — zaczyna się od pytania, czyim kluczem i dla kogo ten podpis powstał, a kończy na tym, czy da się token unieważnić, zanim wygaśnie sam z siebie. Trzy decyzje konfiguracyjne — zamknięta lista algorytmów, wymagane iss i aud oraz kid traktowany jak etykieta — zamykają większość realnych ścieżek ataku. Reszta to dyscyplina operacyjna wokół rotacji i logów.


Źródła pierwotne: RFC 7519 — JSON Web Token, RFC 8725 — JWT Best Current Practices, OWASP — JSON Web Token Cheat Sheet.

UDOSTĘPNIJ / KOPIUJ