Jak działa ta kontrola
Scanaris czyta JavaScript Twojej strony głównej, łącznie ze skryptami ładowanymi z Twojej domeny, i szuka wywołań localStorage.setItem lub sessionStorage.setItem, których klucz sugeruje poświadczenie: token, jwt, auth, secret, password, api_key, access, refresh lub bearer. Jeśli znajdzie takie wywołanie, otwiera znalezisko o niskiej wadze. Niska, bo trzymanie tokena w przeglądarce samo w sobie nie jest włamaniem; staje się nim w połączeniu z jakimkolwiek wstrzyknięciem skryptu, i dlatego warto o tym wiedzieć.
Dlaczego to ważne
Wszystko w localStorage może odczytać dowolny kod działający na Twojej stronie: Twoje skrypty, skrypty zewnętrzne, które ładujesz (analityka, czaty, widżety), i każdy skrypt, który atakujący zdoła wstrzyknąć. Jedna podatność XSS, w Twoim kodzie lub w zależności, wystarczy, żeby localStorage.getItem('token') powędrował na obcy serwer i ktoś użył sesji Twojego użytkownika z innego miejsca, bez jego hasła i bez jego wiedzy. Ciasteczko z HttpOnly jest niewidoczne dla JavaScriptu, więc ten sam XSS nie może go ukraść; może wykonywać żądania, dopóki strona jest otwarta, co jest złe, ale nie zabierze sesji.
Skąd to zwykle pochodzi
Z domyślnej konfiguracji wielu bibliotek uwierzytelniania w aplikacjach jednostronicowych. Klient Supabase dla przeglądarki trzyma sesję w localStorage, jeśli nie powiesz mu inaczej, podobnie jak wiele przykładów JWT „na piechotę" w Expressie. Lovable, Bolt i v0 generują ten wzorzec, bo to ten z dokumentacji szybkiego startu.
Jak to naprawić
- Sesja w ciasteczkach HttpOnly. Jeśli masz serwer (Next.js, Nuxt, Express, Edge Function), niech to on odbiera token od dostawcy i zapisuje go w ciasteczku z
HttpOnly,SecureiSameSite=Lax. Przeglądarka wysyła je sama i nigdy go nie widzi. - Z Supabase: w Next.js lub Nuxt użyj
@supabase/ssr, które trzyma sesję w ciasteczkach; w czystej SPA bez serwera zaakceptuj ryzyko i zrekompensuj je ścisłym CSP, bo nie ma innego miejsca do przechowania. - Krótkie tokeny i bezpieczne odświeżanie. Jeśli token dostępu musi żyć w pamięci lub w storage, niech wygasa po minutach, a token odświeżania niech idzie w ciasteczku
HttpOnly. - Pamięć zamiast storage. Trzymanie tokena w zmiennej JavaScript (nie w storage) chroni go przed skryptami czytającymi storage, choć nie przed aktywnym XSS na tej samej stronie. Lepiej niż localStorage, gorzej niż ciasteczko.
- Zmniejsz powierzchnię XSS. CSP bez
'unsafe-inline'wscript-src, oczyszczanie każdego HTML-a od użytkownika i przegląd ładowanych skryptów zewnętrznych. To obrona, dzięki której token w storage przestaje mieć znaczenie.
Sprawdź, czy naprawione
curl -s https://twojadomena.com | grep -oE 'src="[^"]+\.js[^"]*"' | cut -d'"' -f2 | head -3
curl -s https://twojadomena.com/assets/index-XXXX.js | grep -oE "(local|session)Storage\.setItem\([^)]{0,60}" | head
Jeśli drugie polecenie zwraca linie z token, auth lub podobnym w kluczu, wzorzec wciąż tam jest. W narzędziach przeglądarki, zakładka Aplikacja, zobaczysz, co Twoja aplikacja trzyma w storage po zalogowaniu.
Często zadawane pytania
Supabase domyślnie trzyma sesję w localStorage. Czy to źle?
To kompromis, na który idzie SPA bez serwera. Jeśli masz serwer, przejdź na @supabase/ssr. Jeśli nie, dbaj o CSP i skrypty zewnętrzne: ten token jest tak bezpieczny jak strona wokół niego.
Czy sessionStorage jest bezpieczniejszy niż localStorage?
Tylko pod względem czasu życia: kasuje się przy zamknięciu karty. Każdy skrypt na stronie czyta go tak samo.
Dlaczego niska waga, skoro może ukraść sesję?
Bo do wykorzystania potrzebna jest inna podatność (XSS). Kontrola ostrzega, żebyś decydował z wiedzą; jeśli Twoja strona ma już ścisłe CSP i nie pokazuje nieoczyszczonej treści użytkowników, realne ryzyko jest małe.