Voltar ao blog
Segurança 21 de abril de 2026 6 min de leitura

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

http
Set-Cookie: session=eyJhbGc...; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=3600

O 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