Programação Por Equipe DevBras

Capa do artigo: Domínio do cookie: login que some entre www e apex ou vaza para subdomínio

Domínio do cookie: login que some entre www e apex ou vaza para subdomínio

Entenda quando o cookie vale só para o host e quando vale para todo subdomínio, escolha o escopo certo da sessão e use o prefixo __Host- para travar.

A pessoa entra em www.lojamodelo.com.br, faz login, clica num link de e-mail que aponta para lojamodelo.com.br e aparece deslogada. Alguém "conserta" colocando Domain=lojamodelo.com.br no cookie de sessão, e o login passa a funcionar nos dois. Também passa a ser enviado para blog.lojamodelo.com.br, hospedado num serviço de terceiros, e para homolog.lojamodelo.com.br, que roda uma versão de seis meses atrás. A resposta curta: sem o atributo Domain, o cookie volta só para o host exato que o criou; com Domain, ele vai para aquele domínio e todos os subdomínios. Para sessão, deixe o cookie restrito ao host, escolha um único endereço oficial para o site e redirecione o outro para ele.

O prefixo __Host- no nome do cookie trava essa decisão: o navegador só aceita o cookie se ele for Secure, tiver Path=/ e não tiver Domain.

Como o navegador decide para onde o cookie vai

Quando o servidor manda Set-Cookie, o navegador guarda o cookie junto com um escopo. Em cada pedido seguinte, ele confere se o endereço de destino cabe nesse escopo antes de anexar o cookie. Dois atributos definem o escopo de endereço:

  • Domain ausente: cookie host-only. Vale só para o host exato da resposta. Criado em www.lojamodelo.com.br, não vai para lojamodelo.com.br nem para api.lojamodelo.com.br.
  • Domain=lojamodelo.com.br: vale para esse domínio e para qualquer subdomínio dele, em qualquer profundidade. Um ponto no começo do valor é ignorado pelos navegadores atuais; não muda nada.
  • Path: restringe pelo caminho da URL. Não é fronteira de segurança, porque páginas do mesmo host conseguem ler cookies de outros caminhos por outros meios; serve para organizar, não para isolar.

Há limites que o navegador impõe. Um site não consegue definir Domain para outro domínio que não seja o dele ou um pai dele, e não consegue definir para um sufixo público como com.br. A lista de sufixos públicos (Public Suffix List) é o que impede lojamodelo.com.br de criar cookie para todo .com.br.

O lado que costuma ser esquecido: subdomínio escrevendo cookie

A regra vale nos dois sentidos. Qualquer página em blog.lojamodelo.com.br pode emitir um cookie com Domain=lojamodelo.com.br, e esse cookie chega à loja principal. Se o subdomínio for comprometido ou estiver sob controle de terceiros, ele pode plantar um cookie com o mesmo nome da sua sessão e tentar fixar uma sessão conhecida. O prefixo __Host- fecha essa porta, porque um cookie com esse prefixo nunca pode ter Domain, então nenhum subdomínio consegue criá-lo para o domínio pai.

Vale lembrar que SameSite não ajuda aqui. Para o navegador, todos os subdomínios do mesmo domínio registrável são o mesmo site, então SameSite=Strict ainda envia o cookie entre blog e www. Os atributos HttpOnly, Secure e SameSite estão detalhados em cookie de sessão com HttpOnly, Secure e SameSite; este post trata do escopo de domínio.

Como escolher o escopo da sessão

  1. Liste todos os hosts do seu domínio: site, API, painel, blog, homologação, ferramentas de marketing, subdomínios antigos que ainda resolvem no DNS.
  2. Escolha o endereço oficial do site, com ou sem www, e redirecione o outro com 301 para ele, preservando caminho e query.
  3. Emita o cookie de sessão sem Domain e com o prefixo __Host-, Secure, HttpOnly e Path=/.
  4. Se a API está em outro subdomínio, prefira servi-la no mesmo host, num caminho como /api, a ampliar o cookie para o domínio inteiro.
  5. Se precisar mesmo compartilhar entre subdomínios, use um domínio separado só para hosts que você controla, como app-lojamodelo.com.br, em vez de abrir o domínio onde vivem blog e ferramentas de terceiros.
  6. Teste nos dois nomes e num subdomínio que não deveria receber o cookie.

O cookie de sessão no Express

app.post('/login', async (req, res) => {
  const sessao = await criarSessao(req.body.email, req.body.senha);
  if (!sessao) return res.status(401).send('Credenciais inválidas');

  res.cookie('__Host-sessao', sessao.id, {
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
    path: '/',
    maxAge: 8 * 60 * 60 * 1000,
  });
  res.redirect('/conta');
});

Não há opção domain no objeto, então o cookie é host-only. O cabeçalho que sai fica parecido com este:

Set-Cookie: __Host-sessao=k3f9...; Max-Age=28800; Path=/; Expires=...; HttpOnly; Secure; SameSite=Lax

Se alguém adicionar domain no futuro, o navegador rejeita o cookie por causa do prefixo, e o login quebra em teste em vez de vazar em produção. É um erro barulhento de propósito.

O redirecionamento para um único endereço

No nginx, o host que não é oficial só redireciona:

server {
    listen 443 ssl;
    server_name lojamodelo.com.br;
    ssl_certificate     /etc/letsencrypt/live/lojamodelo.com.br/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/lojamodelo.com.br/privkey.pem;
    return 301 https://www.lojamodelo.com.br$request_uri;
}

O certificado precisa cobrir os dois nomes, senão o navegador mostra erro antes do redirecionamento acontecer. Os detalhes de código de status e de como não perder caminho e parâmetros estão em redirecionar URL do jeito certo.

Como conferir o que o navegador guardou

No Chrome ou no Edge, abra as ferramentas de desenvolvedor, aba Application, seção Cookies. A coluna Domain mostra o host exato quando o cookie é host-only, e o domínio, às vezes com um ponto na frente, quando foi definido com Domain. No Firefox, a aba equivalente é Storage. Pelo terminal, curl -sI num endpoint que emite cookie mostra o Set-Cookie cru, e basta procurar Domain= nele.

Depois, visite um subdomínio que não deveria receber a sessão e confira, na aba Network, se o cabeçalho Cookie do pedido traz o cookie. Esse teste costuma revelar o subdomínio esquecido.

Erros comuns

  • Usar Domain para resolver o problema www versus apex. O login passa a funcionar nos dois e a sessão vai junto para todo subdomínio, inclusive os de terceiros.
  • Manter os dois endereços servindo o site. Cada um tem seus cookies, a pessoa parece deslogada ao trocar de nome e a busca indexa duas cópias.
  • Esquecer subdomínios antigos no DNS. Um host abandonado que aponta para serviço de terceiros pode ser tomado e passar a receber cookies do domínio pai.
  • Achar que SameSite isola subdomínios. Subdomínios do mesmo domínio registrável são o mesmo site, e o cookie é enviado normalmente.
  • Usar Path como isolamento entre aplicações. Duas aplicações no mesmo host conseguem acessar os cookies uma da outra.
  • Homologação no mesmo domínio da produção com cookie amplo. A versão de teste recebe a sessão real de quem visitou os dois.

Checklist

  • O cookie de sessão não tem atributo Domain.
  • O nome do cookie de sessão usa o prefixo __Host-.
  • Existe um único endereço oficial e o outro redireciona com 301.
  • O certificado cobre os dois nomes.
  • A lista de subdomínios do DNS foi revisada e os abandonados removidos.
  • Homologação e ferramentas de terceiros não ficam sob um domínio que recebe a sessão.
  • Um teste manual confirma que um subdomínio qualquer não recebe o cookie.

Abra a aba de cookies no site de produção agora: a sessão aparece com o host exato ou com o domínio inteiro, e quantos subdomínios no seu DNS a receberiam hoje?

Avalie este artigo

Referência: https://developer.mozilla.org/docs/Web/HTTP/Cookies#define_where_cookies_are_sent