Como funciona esta verificação
O Scanaris lê o JavaScript da tua página principal, incluindo os scripts que carrega a partir do teu domínio, e procura chamadas a localStorage.setItem ou sessionStorage.setItem cuja chave sugira uma credencial: token, jwt, auth, secret, password, api_key, access, refresh ou bearer. Se encontrar uma, abre um achado de severidade baixa. É baixa porque guardar um token no navegador não é uma brecha por si só; torna-se numa quando se combina com qualquer injeção de script, e por isso vale a pena sabê-lo.
Porque importa
Tudo o que está no localStorage pode ser lido por qualquer código que corra na tua página: os teus scripts, os de terceiros que carregas (analítica, chats, widgets) e qualquer script que um atacante consiga injetar. Uma única vulnerabilidade XSS, no teu código ou numa dependência, basta para que localStorage.getItem('token') viaje para um servidor alheio e alguém use a sessão do teu utilizador a partir de outro sítio, sem a palavra-passe dele e sem que ele repare. Um cookie com HttpOnly é invisível para o JavaScript, por isso esse mesmo XSS não o consegue roubar; pode fazer pedidos enquanto a página está aberta, o que é mau, mas não levar a sessão.
De onde vem normalmente
Da configuração por defeito de muitas bibliotecas de autenticação em aplicações de página única. O cliente do Supabase para o navegador guarda a sessão em localStorage se não lhe disseres outra coisa, tal como muitos exemplos de JWT "feito à mão" com Express. O Lovable, o Bolt e o v0 geram esse padrão porque é o que aparece na documentação de início rápido.
Como corrigir
- Sessão em cookies HttpOnly. Se tens servidor (Next.js, Nuxt, Express, uma Edge Function), que seja ele a receber o token do fornecedor e a guardá-lo num cookie com
HttpOnly,SecureeSameSite=Lax. O navegador envia-o sozinho e nunca o vê. - Com Supabase: em Next.js ou Nuxt usa
@supabase/ssr, que guarda a sessão em cookies; numa SPA pura sem servidor, aceita o risco e compensa-o com uma CSP estrita, porque não há outro sítio onde a guardar. - Tokens curtos e renovação segura. Se o token de acesso tem de viver em memória ou no storage, que expire em minutos e que o de renovação vá num cookie
HttpOnly. - Memória em vez de storage. Guardar o token numa variável de JavaScript (não no storage) protege-o de scripts que leiam o storage, embora não de um XSS ativo na mesma página. Melhor do que localStorage, pior do que um cookie.
- Reduz a superfície de XSS. Uma CSP sem
'unsafe-inline'emscript-src, sanitizar qualquer HTML de utilizador e rever os scripts de terceiros que carregas. É a defesa que faz com que o token no storage não importe.
Confirma que está corrigido
curl -s https://oteudominio.com | grep -oE 'src="[^"]+\.js[^"]*"' | cut -d'"' -f2 | head -3
curl -s https://oteudominio.com/assets/index-XXXX.js | grep -oE "(local|session)Storage\.setItem\([^)]{0,60}" | head
Se o segundo comando devolver linhas com token, auth ou semelhantes na chave, o padrão continua lá. Nas ferramentas do navegador, separador Aplicação, podes ver o que a tua app guarda no storage depois de iniciar sessão.
Perguntas frequentes
O Supabase guarda a sessão em localStorage por defeito. Está mal?
É o compromisso que uma SPA sem servidor assume. Se tens servidor, muda para @supabase/ssr. Se não, cuida da CSP e dos scripts de terceiros: esse token é tão seguro como a página que o rodeia.
O sessionStorage é mais seguro do que o localStorage?
Só na duração: apaga-se ao fechar o separador. Qualquer script da página lê-o na mesma.
Porque é severidade baixa se pode roubar a sessão?
Porque precisa de outra vulnerabilidade (um XSS) para ser explorado. O check avisa-te para decidires com informação; se o teu site já tem uma CSP estrita e não mostra conteúdo de utilizador sem sanitizar, o risco real é pequeno.