Powrót do bloga
28 lipca 2026

Nasza własna aplikacja trzymała tokeny uwierzytelniające w localStorage. Co z tym zrobiliśmy.

Viktor Bulanek
Founder & CTO, Penetrify
MSc IT Security · 20+ years in security · 4x Ex-CTO

W czerwcu skierowaliśmy Penetrify na naszą własną aplikację, tak jak robimy to przed większym wydaniem. Skan wrócił z problemem w naszym logowaniu przez Cognito, a kiedy pociągnąłem za ten wątek, wyszło z tego coś, czego nie czytałem z przyjemnością: dashboard Penetrify trzymał tokeny access i refresh z Cognito w localStorage, w postaci jawnej, czytelne dla dowolnego JavaScriptu działającego na stronie.

Nie usiedliśmy i tak nie zdecydowaliśmy. To domyślne zachowanie biblioteki uwierzytelniania, której użyliśmy, i nigdy go nie zakwestionowaliśmy. Właśnie o tym warto napisać.

Czym dokładnie było to znalezisko

Nasz dashboard to aplikacja single-page. Po logowaniu dostawała tokeny z AWS Cognito, a SDK umieszczało je w pamięci przeglądarki, bo tam je umieszczają SDK, kiedy w sesji nie bierze udziału serwer. Przeczytać je mógł każdy skrypt z dostępem do strony.

To znaczy: jeden błąd cross-site scripting gdziekolwiek w aplikacji, jedno złośliwe rozszerzenie przeglądarki albo jedna skompromitowana zależność npm, i atakujący wychodzi z oboma tokenami. Token access wpuszcza go na konto. Token refresh go tam utrzymuje, bo pozostaje ważny długo po tym, jak sesja wygląda na zamkniętą, dopóki coś go wyraźnie nie unieważni.

Dla naszych użytkowników w zasięgu były dane rozliczeniowe, wyniki skanów ich aplikacji i dane o wypłatach afiliacyjnych. Na ile potrafimy to ocenić, nikt tego nie wykorzystał. Ale nie o to chodzi. Było osiągalne i to była nasza własna domyślna wartość do naprawy.

Dlaczego szyfrowanie localStorage nie jest rozwiązaniem

Pierwszy odruch to zaszyfrować tokeny przed zapisem. Nie pomaga. Klucz deszyfrujący musi żyć w tym samym JavaScripcie, który atakujący już wykonuje. Dodałeś krok, nie granicę.

Drugi odruch to przenieść tokeny do ciasteczka po stronie klienta. To też nie pomaga. Ciasteczko ustawione przez JavaScript jest przez JavaScript czytelne. Właściwość, która ma znaczenie, to HttpOnly, a ustawić ją może tylko serwer, nagłówkiem Set-Cookie. Jeśli twoja poprawka działa w przeglądarce, to nie jest poprawka.

Co wdrożyliśmy zamiast tego

Całą sesję przenieśliśmy do ciasteczek ustawianych przez serwer, wzorcem token handler wewnątrz backendu, który już mieliśmy:

  • Backend FastAPI wykonuje wywołania do Cognito i zwraca sesję jako ciasteczka z HttpOnly, Secure i SameSite. Przeglądarka nigdy nie widzi tokenu.
  • Middleware uwierzytelniania czyta token access z ciasteczka, a nie z nagłówka Authorization.
  • Ponieważ ciasteczka wysyłają się automatycznie, żądania modyfikujące niosą teraz nagłówek z tokenem CSRF, który backend weryfikuje.
  • Wylogowanie wykonuje global sign-out po stronie Cognito, więc token, który kiedyś wyciekł, umiera razem z sesją, zamiast ją przeżyć.
  • SDK uwierzytelniania zniknęło z przeglądarki całkowicie.

Nie potrzebowaliśmy do tego nowej usługi. Nasza statyczna aplikacja i nasze API są już na tym samym originie: CloudFront serwuje aplikację i kieruje /api/* do API Gateway. Ciasteczko first-party jest więc wysyłane przy każdym wywołaniu API bez dodatkowej infrastruktury.

Najpierw przyjrzeliśmy się ścieżce rekomendowanej przez dostawcę, co oznaczałoby przejście na hostowany interfejs logowania, i odrzuciliśmy ją. Wyrzuciłaby nasze własne ekrany logowania, MFA i resetu hasła, żeby rozwiązać problem, który potrafimy rozwiązać w istniejącym backendzie.

Ile to kosztowało

Około tygodnia pracy i żadnej widocznej zmiany dla użytkowników. To uczciwe podsumowanie większości pracy nad bezpieczeństwem i dokładnie dlatego takie rzeczy w startupie odkłada się w nieskończoność. Nic na roadmapie się nie rusza. Żaden klient o to nie prosi. Zmienia się tylko to, co jeden wstrzyknięty skrypt może zrobić twoim użytkownikom.

Jeden praktyczny szczegół, który uczynił wdrożenie bezpiecznym: podczas rolloutu backend nadal przyjmował starą ścieżkę przez nagłówek, więc cofnięcie deployu frontendu było rollbackiem w jednym kroku, gdyby cokolwiek poszło nie tak.

Sprawdź własną aplikację. Zajmie to około 30 sekund.

Otwórz swoją aplikację, zaloguj się, potem otwórz DevTools:

  • Zakładka Application, Local Storage. Szukaj długich wartości z dwiema kropkami. Ten kształt to JWT.
  • W tym samym panelu sprawdź Session Storage. Ten sam problem, krótsze życie.
  • W konsoli wpisz document.cookie. Cokolwiek się tam pojawi, jest z definicji czytelne dla JavaScriptu, więc ciasteczko sesji, które widzisz w tym wyniku, cię nie chroni.

Jeśli znajdziesz token, to jest znalezisko. Nie teoretyczne i nie takie, o którym powiedziałby ci zielony audyt zależności albo czysty przebieg analizy statycznej.

Dlaczego to publikujemy

Dwa powody, a drugi dotyczy naszego własnego produktu.

Pierwszy: domyślne ustawienia to decyzje, które podjął za ciebie ktoś inny. Przychodzą z biblioteką, są wygodne i prawie nigdy nie są modelem zagrożeń. Nasze było rozsądnym domyślnym ustawieniem dla tutorialu i złym dla aplikacji trzymającej dane płatnicze.

Drugi: skan jest wskazówką, nie wyrokiem. Nasz oznaczył przepływ logowania i dowiódł, że jest osiągalny. Nie podał nam zdania „twoja biblioteka uwierzytelniania ma domyślnie localStorage i właśnie to trzeba zmienić". Droga od znaleziska do poprawki oznaczała otwarcie własnej ścieżki uwierzytelniania i przeczytanie jej. Dokładnie tak oczekujemy, że będziesz używać naszych raportów: znalezisko mówi, gdzie patrzeć, i pokazuje żądanie, które to dowodzi, a ty decydujesz, co jest właściwą poprawką w twojej architekturze.

Więc: uruchom skan na własnej aplikacji, a potem przeczytaj kod, na który wskazuje. Zrobiliśmy jedno i drugie, i żadna z tych połówek nie wystarczyłaby sama.

Jeśli chcesz spojrzenie na swoją aplikację z zewnątrz od razu, nasz darmowy security check zajmuje około 60 sekund i nie wymaga rejestracji. Jeśli chcesz pełny raport z dowodami i poprawkami, pierwszy skan kosztuje $29 i zalicza się na poczet planu. A jeśli wolisz tę darmową rzecz, którą właśnie opisaliśmy, otwórz DevTools i zajrzyj do swojego local storage. Ta nie kosztuje nic.

Frequently Asked Questions

Jakie typy podatności wykrywa Penetrify?

Penetrify wykrywa wszystkie kategorie podatności OWASP Top 10, w tym SQL injection, XSS, CSRF, IDOR, złamaną autentykację, błędne konfiguracje zabezpieczeń i ujawnianie wrażliwych danych. Testuje również bezpieczeństwo API, zarządzanie sesją oraz typowe błędy konfiguracji w Supabase, Firebase i Bubble.

Jak długo trwa test penetracyjny AI?

Szybkie skanowanie kończy się w 15–30 minut. Standardowe skanowanie trwa 1–2 godziny z szerszym zakresem. Głęboke skanowanie może trwać kilka godzin dla złożonych aplikacji.

Co zawiera raport Penetrify?

Każdy raport zawiera podsumowanie wykonawcze, ogólny wynik bezpieczeństwa, znaleziska sklasyfikowane według wagi (Krytyczne, Wysokie, Średnie, Niskie), szczegółowe kroki reprodukcji oraz konkretne wskazówki dotyczące naprawy napisane dla deweloperów – nie dla specjalistów ds. zgodności.

Related articles

Alternatywy DAST na rok 2026: Kiedy skanowanie dynamiczne nie wystarcza (i czego użyć zamiast)
Skanery DAST pomijają przepływy uwierzytelniania, logikę biznesową oraz nowoczesne API. Oto uczciwe porównanie DAST z SAST, IAST, PTaaS oraz autonomicznego Penetration Testing z wykorzystaniem AI – i kiedy stosować każde z nich.
What an autonomous pentest agent found in 3,847 apps — and what your scanner didn't
A data breakdown of 47,291 exploitation-validated findings, with methodology and limitations. 91% of the SQL injection we found shipped despite a SAST gate in CI; 78% of critical findings needed no login.
Indirect Prompt Injection to Data Exfiltration: When the Model Has Tools
Prompt injection on its own is a curiosity. Prompt injection reaching a tool that holds real credentials is a breach — and the instruction does not have to come from your user. It can arrive inside the document your app was asked to summarise.

Explore more