Como funciona esta verificação
O Scanaris olha para a resposta principal da tua página e verifica duas condições ao mesmo tempo. Primeira: a resposta estabelece pelo menos um cookie (Set-Cookie) cujo nome começa por sess, sid, auth, token, jwt, connect.sid, phpsessid ou asp.net, ou seja, um cookie que muito provavelmente identifica uma sessão. Segunda: a mesma resposta leva Cache-Control com a diretiva public. Só se ambas se cumprirem abre um achado de severidade média com o nome do cookie e o valor do cabeçalho. É deliberadamente estrito: um cookie de preferências com cache pública, ou um cookie de sessão sem public, não são marcados.
Porque importa
Cache-Control: public é uma instrução explícita a qualquer cache partilhada (uma CDN, um proxy inverso como o Varnish ou o nginx, o proxy de uma empresa ou operadora) de que essa resposta pode ser guardada e entregue a outros utilizadores. Se essa mesma resposta inclui um Set-Cookie de sessão, a cache pode guardar o cookie junto com o corpo e dá-lo ao visitante seguinte. Esse visitante entra com a sessão do anterior, sem palavra-passe, e vê os dados dele. É uma falha das que se descobrem quando um cliente escreve a dizer que "entrou na conta de outra pessoa". Aconteceu em bancos e em lojas grandes, sempre pela mesma combinação: conteúdo personalizado marcado como cacheável.
Porque acontece
Normalmente por uma regra de cache demasiado ampla: um add_header Cache-Control "public, max-age=3600" no nginx aplicado a todas as rotas, incluindo as que criam sessão; uma regra "Cache Everything" no Cloudflare; ou um routeRules com cabeçalhos de cache para '/**' numa app que estabelece cookies na página principal. A maioria das CDN recusa guardar respostas com Set-Cookie por defeito, e o nginx também, salvo que lhe digas para ignorar esse cabeçalho, mas um public explícito és tu a dizer-lhes que o façam.
Como corrigir
- As respostas que criam ou renovam sessão levam
Cache-Control: private, no-store. É o que fazem por defeito oexpress-session, o Laravel, o Django e o Rails; se vêspublic, foi uma camada posterior que o adicionou. - Separa as regras de cache por tipo de rota.
publiccommax-agelongo para os ficheiros estáticos com hash no nome (/_nuxt/,/_next/static/,/assets/);privateouno-storepara o HTML que depende do utilizador. No nginx, usa blocoslocationdistintos; no Nuxt e no Next.js, regras por rota em vez de uma global. - Não estabeleças o cookie de sessão em páginas públicas cacheáveis. Se a home não precisa de sessão, não a crie: cria o cookie ao iniciar sessão ou ao entrar no painel.
- Se usas uma CDN com "Cache Everything" ou semelhante, adiciona uma regra para não fazer cache de respostas com
Set-Cookienem das rotas autenticadas. E adicionaVary: Cookiecomo rede adicional, embora não substitua o anterior.
Confirma que está corrigido
curl -sI https://teudominio.com | grep -iE "^(set-cookie|cache-control|vary):"
curl -sI https://teudominio.com/dashboard | grep -iE "^(set-cookie|cache-control):"
Se houver Set-Cookie com nome de sessão, o Cache-Control dessa resposta deve ser private, no-store ou no-cache, nunca public. Faz o segundo pedido com e sem o teu cookie de sessão: a resposta autenticada nunca deve aparecer sem ele.
Perguntas frequentes
A minha CDN diz que não faz cache de respostas com cookies. Então é falso positivo?
Provavelmente a fuga não está a acontecer hoje, mas dependes de um comportamento por defeito que uma regra nova ou uma mudança de fornecedor podem desativar. O cabeçalho diz que é público; corrige o cabeçalho e deixa de depender da exceção.
E os cookies de analítica ou de consentimento?
Não são marcados: só contam os nomes que indicam sessão. Um cookie de consentimento numa página pública com cache é normal. Como devem estar configurados os cookies de sessão (Secure, HttpOnly, SameSite) vê-o cookies inseguros.
A minha sessão vai num token no localStorage, não em cookie.
Então esta verificação não se aplica, mas token no armazenamento sim: um token acessível a partir do JavaScript tem outros problemas.