Programação Por Equipe DevBras

Capa do artigo: O CDN mostrou a conta de outra pessoa: cache de página logada não é público

O CDN mostrou a conta de outra pessoa: cache de página logada não é público

Marque respostas com sessão como private ou no-store, nunca cacheie Set-Cookie no CDN e separe o miolo público do pedaço personalizado da página.

Terça, 10h15. A loja ligou o cache do CDN na home e o tempo de resposta caiu de 800 ms para 40 ms. Às 10h40 chegou o primeiro chamado: "entrei no site e apareceu 'Olá, Ana' e o carrinho dela". A home de quem estava logado entrou no mesmo cache da home anônima, e o CDN passou a entregar a versão da Ana para todo mundo. A resposta curta: resposta que depende de quem pediu nunca vai para cache compartilhado. Marque essas respostas com Cache-Control: private ou no-store, cacheie no CDN só o que é igual para todos e nunca deixe uma resposta com Set-Cookie ser guardada por um intermediário.

Velocidade e vazamento vieram no mesmo pacote porque o cache não sabia de quem era a página. Ele só sabia a URL, e a URL da home é a mesma para todo mundo.

Por que o CDN entrega a página errada

Um cache compartilhado, seja CDN, proxy reverso ou cache de operadora, guarda a resposta sob uma chave. Na configuração padrão, essa chave é basicamente o método, o host e a URL. O cookie de sessão, que é o que diferencia a Ana do visitante seguinte, normalmente fica de fora.

Quem diz ao cache se ele pode guardar é a sua aplicação, pelo cabeçalho Cache-Control. Se a resposta diz public, max-age=300, o cache entende que pode guardar por cinco minutos e entregar a qualquer um. Muitos vazamentos vêm de um middleware que aplica esse cabeçalho a todas as rotas, ou de uma regra no painel do CDN que força cache por extensão ou caminho, ignorando o que a aplicação mandou.

O Vary: Cookie parece a saída: ele diz que respostas diferentes dependem do cookie. Na prática, CDNs tratam o Vary de formas diferentes, e cada visitante tem um cookie único, então a taxa de acerto do cache vai a zero. Para conteúdo personalizado, a resposta certa é não guardar no intermediário.

Os valores de Cache-Control que importam aqui

  • public, max-age=60, s-maxage=300: qualquer cache pode guardar. O navegador por 60 segundos, caches compartilhados por 300.
  • private, max-age=0: só o navegador da própria pessoa pode guardar; intermediários não.
  • no-store: ninguém guarda, nem o navegador. Para extrato, dados pessoais e páginas de conta em computador compartilhado.
  • no-cache: ao contrário do que o nome sugere, permite guardar, mas exige revalidar com o servidor antes de usar. Não é o que protege dado privado.

Como separar o público do personalizado

  1. Liste as rotas que dependem de sessão: conta, carrinho, pedidos, checkout e qualquer página que mostre nome ou dado da pessoa.
  2. Marque essas respostas como privadas na aplicação, num lugar central que olha se há sessão, e não rota a rota.
  3. Tire o personalizado das páginas públicas. A home fica igual para todos e o "Olá, Ana" e o contador do carrinho chegam por uma chamada separada, como /api/eu, com no-store.
  4. Nunca cacheie respostas com Set-Cookie. Se uma página anônima cria sessão, o cache pode entregar o mesmo cookie de sessão para todos os visitantes.
  5. Configure o CDN para respeitar os cabeçalhos da origem, ou para não cachear quando houver cookie de sessão na requisição.
  6. Não coloque dado pessoal na URL. Token ou e-mail na query vira chave de cache e aparece em log de CDN.
  7. Teste com e sem sessão e compare os cabeçalhos.

O middleware em Express

app.use((req, res, next) => {
  if (req.session && req.session.usuarioId) {
    res.set('Cache-Control', 'private, no-store');
  } else {
    res.set('Cache-Control', 'public, max-age=60, s-maxage=300');
  }
  next();
});

app.get('/api/eu', (req, res) => {
  res.set('Cache-Control', 'no-store');
  if (!req.session || !req.session.usuarioId) return res.json({ logado: false });
  res.json({ logado: true, nome: req.session.nome, itensNoCarrinho: req.session.itens || 0 });
});

A regra fica num lugar só, antes das rotas, e o padrão para quem tem sessão é o mais seguro. Rotas anônimas que criam sessão, como o primeiro acesso que gera cookie, também precisam sair do cache público: confira se a sua biblioteca de sessão só cria o cookie quando há algo para guardar.

O mesmo cuidado no Nginx como cache

proxy_cache_path /var/cache/nginx/loja keys_zone=loja:10m max_size=1g inactive=60m;

server {
    listen 443 ssl;
    server_name loja.exemplo.com.br;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_cache loja;
        proxy_cache_bypass $cookie_sessao;
        proxy_no_cache $cookie_sessao;
        add_header X-Cache-Status $upstream_cache_status;
    }
}

Com o cookie sessao presente, o Nginx nem consulta (proxy_cache_bypass) nem grava (proxy_no_cache) a resposta no cache. Por padrão, o Nginx também não guarda respostas com Set-Cookie nem com Cache-Control private ou no-store, a menos que alguém tenha mandado ignorar esses cabeçalhos com proxy_ignore_headers. Se encontrar essa diretiva na configuração, desconfie dela primeiro.

Como conferir

curl -sI https://loja.exemplo.com.br/ | grep -i -E "cache-control|age|set-cookie|x-cache"
curl -sI -H "Cookie: sessao=teste123" https://loja.exemplo.com.br/minha-conta | grep -i -E "cache-control|age|x-cache"

A primeira chamada deve mostrar public e nenhum Set-Cookie. A segunda deve mostrar private ou no-store, e nenhum Age, que indicaria resposta servida do cache. Cada CDN tem seu próprio cabeçalho de diagnóstico, como o CF-Cache-Status do Cloudflare; procure o do seu na documentação dele.

Se o vazamento já aconteceu

Primeiro, pare a fonte: corrija o cabeçalho ou desligue a regra de cache do CDN. Depois, purgue o cache das rotas afetadas, porque as cópias erradas continuam lá até expirar. Se o cache guardou respostas com Set-Cookie, invalide as sessões criadas no período, já que várias pessoas podem ter recebido o mesmo identificador. Por fim, avalie o que foi exposto e para quantas pessoas; dado pessoal exposto pode ter obrigações de comunicação, e o ponto de partida é LGPD num sistema pequeno.

Erros comuns

  • Middleware que aplica public a tudo. Uma rota logada esquecida basta para o vazamento.
  • Regra no CDN que força cache por caminho. Ela passa por cima do que a aplicação disse e cacheia a página de conta.
  • Cachear resposta com Set-Cookie. Vários visitantes recebem o mesmo cookie de sessão e passam a compartilhar a conta.
  • Confiar no Vary: Cookie. Suporte inconsistente entre CDNs e cache que nunca acerta.
  • Achar que no-cache impede o armazenamento. Ele só exige revalidação; a cópia continua guardada.
  • Dado pessoal na query string. Vira parte da chave de cache e aparece em logs de terceiros.

Checklist

  • Toda resposta com sessão sai com private ou no-store.
  • A decisão fica num middleware central, não espalhada por rota.
  • Páginas públicas não mostram dado da pessoa; o personalizado vem por chamada separada.
  • Nenhuma resposta com Set-Cookie é guardada pelo CDN.
  • Não há proxy_ignore_headers nem regra de CDN que force cache em rota logada.
  • URLs não carregam token, e-mail ou documento.
  • Você testou com curl as mesmas rotas com e sem cookie de sessão.

Faça agora o teste do curl com cookie numa rota da área logada do seu site: o cabeçalho que voltou diz que a resposta é privada, ou é igual ao da home anônima?

Avalie este artigo

Referência: https://developer.mozilla.org/docs/Web/HTTP/Headers/Cache-Control