Por que LocalStorage é inseguro para credenciais e tokens
Análise técnica de por que armazenar tokens JWT, refresh tokens ou API keys no LocalStorage do navegador é vetor crítico para XSS. Alternativas práticas para SPA e Next.js.
A pergunta aparece em quase todo projeto frontend: "onde guardo o token de autenticação?". A resposta default — LocalStorage — é confortável, simples e errada do ponto de vista de segurança. Vamos entender por quê e o que fazer no lugar.
O modelo de ameaça
LocalStorage é acessível por qualquer JavaScript rodando no mesmo origin. Isso inclui:
- Scripts XSS injetados via formulários, comentários, URLs com vulnerabilidade reflexiva
- Dependências npm maliciosas que sub-repticiamente leem window.localStorage
- Browser extensions com permissão para o seu domínio
- Scripts de analytics, ads ou widgets de terceiros que rodam no mesmo origin
Diferente de cookies HttpOnly (que JavaScript não consegue ler), tudo em LocalStorage é leitura trivial: `localStorage.getItem("token")` é uma linha. Um atacante que conseguir executar JS na sua página tem o token na hora.
O contra-argumento ruim: "mas eu sanitizo XSS"
Esse é o ponto em que devs experientes erram. XSS é uma classe de vulnerabilidade com superfície enorme: cada `dangerouslySetInnerHTML`, cada `innerHTML`, cada renderização de markdown não-sanitizado, cada URL refletida — é um vetor potencial. Frameworks modernos (React, Vue) mitigam por padrão, mas a primeira lib que você adicionar com bug de XSS abre tudo.
A premissa de segurança madura é: assuma que XSS vai acontecer. Projete a arquitetura para limitar o blast radius. Cookies HttpOnly + SameSite Strict cumprem esse papel para autenticação; LocalStorage não.
O que usar em vez disso
Para tokens de sessão: Cookies HttpOnly + SameSite + Secure
Set-Cookie: session=eyJhbGc...; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=3600O cookie é enviado automaticamente pelo navegador em requisições para o mesmo origin. JavaScript não consegue ler. Combinado com SameSite Strict (ou Lax), bloqueia CSRF.
Para refresh tokens: rotação curta + revogação no servidor
Refresh tokens devem viver em cookie HttpOnly separado, com path restrito a `/auth/refresh`. Rotacione a cada uso (single-use refresh tokens) — se o atacante usar o token, o usuário legítimo é deslogado e você detecta a invasão.
Para API keys de cliente: token de curtíssima duração + proxy
API keys longas (Stripe public, Mapbox, etc) embedadas no frontend são aceitáveis se forem domain-locked no provider. Para chamadas mais sensíveis (OpenAI, Twilio), nunca exponha — proxy via seu backend, que obtém a credencial do Secret Manager por workload identity.
E SessionStorage?
Idêntico ao LocalStorage do ponto de vista de XSS. A única diferença é que limpa ao fechar a aba. Não muda o modelo de ameaça.
Como Single Page Apps (SPA) lidam com isso
O padrão atual recomendado pelo OWASP para SPAs é o BFF (Backend for Frontend) pattern: o backend mantém a sessão real em cookie HttpOnly, e expõe ao frontend apenas as APIs necessárias. O frontend nunca toca em tokens JWT, refresh tokens ou API keys — só faz chamadas autenticadas via cookie automaticamente.
Em Next.js (App Router) isso fica natural: Server Components fazem as chamadas autenticadas usando o cookie da sessão; Client Components nunca veem o token. O App Router foi desenhado pra esse padrão.
Resumo prático
- Tokens de sessão: cookies HttpOnly + Secure + SameSite Strict
- Refresh tokens: cookies HttpOnly com path restrito + rotação single-use
- API keys longas no frontend: só se domain-locked no provider
- API keys sensíveis: proxy via backend que pega do Secret Manager
- LocalStorage/SessionStorage para tokens: nunca