Skip to content
This page has been auto-translated and may contain errors.View in English

Sessões e cookies

Deixe sua mochila em um armário de academia e você recebe uma chave numerada. A chave não diz o que tem dentro, não vale nada sozinha, e não serve para nada a quem estiver em qualquer lugar que não seja aquele vestiário.

Isso é um session cookie. A parte valiosa fica com o servidor; o navegador carrega apenas um número.

O que cada lado guarda

O servidor mantém uma session: um pequeno conjunto de dados representando uma pessoa logada.

js
{
  sessionId: 'a3f9c2e7b418',
  userId: 4821,
  role: 'teacher',
  expires: '2026-09-05T14:30:00Z',
}

Isso é o armário. Ele nunca sai do servidor.

O navegador recebe um cookie carregando o id da sessão e nada mais:

js
res.cookie('sid', 'a3f9c2e7b418', { maxAge: 1800000 })

Essa é a chave. O navegador a armazena sem que ninguém peça e a envia automaticamente em toda requisição para aquele domínio, então continuar logado não exige nenhum código da sua parte.

JunoO que cada lado guarda A parte automática costuma surpreender. Você não escreve nada para anexar o cookie à próxima requisição; o navegador faz isso porque é exatamente o que navegadores fazem com cookies.

O que é conveniente, e também é o motivo de um cookie enviado ao lugar errado ser um problema tão sério. Ele vai para todo lugar para onde aquele domínio vai.

JunoO que cada lado guarda Em uma aplicação Express você raramente constrói qualquer um dos dois objetos na mão. O express-session cria a sessão, a armazena, define o cookie e te entrega req.session para ler e escrever.

E é exatamente por isso que os erros abaixo ainda acontecem. O framework decide onde as sessões ficam e como os cookies são configurados; você decide o que vai dentro deles e por quanto tempo duram.

JunoO que cada lado guarda O id da sessão tem um requisito que costuma passar despercebido: ele precisa vir de uma fonte aleatória criptograficamente segura. crypto.randomBytes atende a esse requisito, Math.random não, e um id que qualquer pessoa consiga prever é uma conta que qualquer pessoa consegue ocupar sem senha.

O cookie também precisa de três atributos que o framework não vai escolher por você. HttpOnly impede que o JavaScript o leia, de modo que um payload de XSS não consiga roubar a sessão.

Secure mantém o cookie fora do HTTP puro. SameSite controla se ele acompanha requisições entre sites, o que é a defesa contra cross-site request forgery: outro site fazendo uma requisição à qual o seu navegador anexa o cookie.

Não presuma um padrão para esse último atributo. O Chromium trata um SameSite não definido como Lax; o Firefox não faz isso, e o bug 1617609 da Mozilla foi resolvido como WONTFIX com a justificativa "quando nenhum atributo SameSite é definido, usamos None por padrão". A proteção do Safari vem do bloqueio de cookies de terceiros em vez disso. Defina esse atributo explicitamente, e lembre-se de que SameSite=None exige Secure.

O ciclo completo do login

Seis passos, e apenas os dois primeiros envolvem uma senha:

  1. O usuário envia um nome de usuário e uma senha.
  2. O servidor os confere com um usuário armazenado.
  3. Em caso de sucesso, o servidor cria uma sessão contendo o id do usuário e o que mais for necessário, como um papel (role).
  4. O servidor devolve um cookie contendo o id da sessão.
  5. O navegador o armazena e passa a anexá-lo a toda requisição futura para aquele domínio.
  6. O servidor lê o id, procura a sessão correspondente e sabe quem está fazendo a requisição.

O passo seis se repete durante toda a vida da sessão. Essa consulta é o que caracteriza esse modelo por completo: o servidor é consultado a cada vez, então ele pode mudar de ideia a qualquer momento.

JunoO ciclo completo do login A senha aparece uma única vez, nos passos um e dois, e nunca mais depois disso.

Tudo que vem depois é o id da sessão fazendo o trabalho, então proteger esse id importa tanto quanto proteger a senha importava antes.

JunoO ciclo completo do login O passo três é onde dados de autorização costumam se infiltrar. Guardar um papel (role) na sessão economiza uma leitura no banco de dados a cada requisição, e isso significa que a aplicação passa a decidir permissões a partir de uma cópia que deixou de ser precisa no instante em que alguém a alterou.

Isso é aceitável para um papel que raramente muda; não é aceitável para um que pode ser revogado durante um incidente. Essa escolha é sua, não do framework.

JunoO ciclo completo do login Falta um passo entre o dois e o três, e deixá-lo de fora é uma vulnerabilidade com nome próprio. Se o navegador já tinha um id de sessão antes do login, e o servidor associa o novo login a esse mesmo id, um atacante que tenha plantado esse id antes agora tem em mãos uma sessão autenticada.

Isso é session fixation, e a correção é regenerar o id no momento em que o privilégio muda: no login, e novamente em qualquer elevação, como para admin. No Express isso é req.session.regenerate(), e a sessão antiga precisa ser destruída, não simplesmente abandonada.

Colocar dados reais nele. A versão tentadora parece útil:

Vulnerable
js
res.cookie('session', {
  sessionId: 'a3f9c2e7b418',
  userId: 4821,
  role: 'admin',
  email: '[email protected]',
  password: 'hunter2',
  ip: '203.0.113.42',
})

Tudo além de sessionId é um risco. Cookies são interceptados, gravados em logs, sincronizados entre dispositivos e lidos por qualquer JavaScript rodando na página. Detalhes privados agora viajam para todo lugar que o usuário vai, e um papel (role) ou id vazado já é o suficiente para ajudar alguém a se passar por ele.

Na STRIDE isso é information disclosure e spoofing; em termos da OWASP, broken access control.

Fixed
js
res.cookie('sid', 'a3f9c2e7b418', { maxAge: 1800000 })

Deixá-lo viver para sempre. Um cookie sem maxAge ou expiração é mantido indefinidamente por muitos navegadores:

Vulnerable
js
res.cookie('sid', 'a3f9c2e7b418')

Um cookie roubado então continua útil pelo tempo que o navegador o mantiver, e alguém cujo acesso deveria ter terminado ainda tem uma chave funcionando. Isso é elevation of privilege, e uma falha de autenticação em termos da OWASP. Trinta minutos é um ponto de partida razoável:

Fixed
js
res.cookie('sid', 'a3f9c2e7b418', { maxAge: 1800000 })
JunoDuas formas de arruinar um cookie As duas correções são uma linha cada. Nenhuma exige infraestrutura nova ou uma biblioteca.

A parte difícil é perceber o problema, porque um cookie cheio de dados úteis e um cookie que nunca expira funcionam perfeitamente até o dia em que param de funcionar.

JunoDuas formas de arruinar um cookie Fique atento às unidades. O maxAge do Express é em milissegundos, enquanto o atributo Max-Age no cabeçalho HTTP é em segundos, então 1800 no lugar errado vira trinta minutos ou menos de dois segundos, dependendo do que você pretendia.

Em uma revisão, a pergunta para qualquer cookie é o que ele guarda e quando ele morre. Duas coisas para checar, e o resto é detalhe.

JunoDuas formas de arruinar um cookie Uma expiração curta e uma boa experiência de usuário não estão em conflito, embora a versão ingênua faça parecer que sim. Sessões rolantes (rolling sessions) estendem a janela conforme há atividade, então alguém trabalhando ativamente continua logado enquanto uma sessão abandonada ainda morre rapidamente.

A combinação que vale conhecer é um timeout de inatividade somado a um limite absoluto: trinta minutos de inatividade, e um teto rígido de oito ou doze horas independentemente disso. Sem o limite absoluto, um cookie roubado que é mantido "aquecido" pelo próprio tráfego do atacante nunca expira.

Duas formas de arruinar uma sessão

Sobrecarregá-la. Uma sessão deveria servir para identificar alguém, mas ela vai acumulando coisas:

Vulnerable
js
{
  sessionId: 'a3f9c2e7b418',
  userId: 4821,
  role: 'teacher',
  cart: [ /* 14 items */ ],
  lastFivePages: [ /* ... */ ],
  theme: 'dark',
  cachedPosts: [ /* ... */ ],
}

Sessões vivem na memória do servidor, então isso custa memória por usuário logado. Pior, os dados ficam desatualizados: a aplicação passa a tomar decisões a partir de uma cópia de dados que o banco de dados já alterou desde então. A STRIDE chama isso de tampering, não porque alguém editou algo, mas porque a decisão se apoia em algo que deixou de ser verdade.

Fixed
js
{
  sessionId: 'a3f9c2e7b418',
  userId: 4821,
  role: 'teacher',
  expires: '2026-09-05T14:30:00Z',
}

Deixá-la viver para sempre. O mesmo erro do cookie, um nível abaixo e ainda pior, porque agora é o próprio servidor que está confiando em dados desatualizados. Uma sessão sem expiração significa que um cookie roubado funciona indefinidamente, que uma permissão revogada nunca chega a ter efeito, e que deslogar alguém de todos os lugares se torna impossível.

JunoDuas formas de arruinar uma sessão Os dois erros de sessão rimam com os erros de cookie, o que ajuda a lembrar deles: mantenha pequena, e dê um fim a ela.

A diferença é que um problema de cookie é visível no navegador, enquanto um problema de sessão fica no servidor, onde ninguém olha.

JunoDuas formas de arruinar uma sessão A regra que mantém as sessões pequenas: guarde identidade, não estado. Um id de usuário e talvez um papel (role). Qualquer coisa que você conseguisse consultar, consulte.

Um carrinho de compras pertence ao banco de dados, onde sobrevive a um reinício do servidor e acompanha o usuário para outro dispositivo. Colocá-lo na sessão geralmente significa perdê-lo bem na pior hora.

JunoDuas formas de arruinar uma sessão O armazenamento de sessão padrão do Express é a memória do próprio processo, e a documentação dele mesma diz claramente que não serve para produção. Ele vaza memória, morre junto com o processo, e não existe para a instância vizinha.

A consequência prática é que um deploy desloga todo mundo, e por trás de um load balancer metade dos seus usuários fica deslogada de forma aleatória. O Redis costuma ser a resposta padrão. Fique atento à versão: o connect-redis v8 exporta RedisStore como uma exportação nomeada, diferente das versões v6 e v7, e isso costuma pegar quem está fazendo upgrade.

Os frameworks lidam bem com a mecânica. Express, Django, Rails e Laravel gerenciam o armazenamento e definem padrões sensatos para os cookies. Nenhum deles decide o que você coloca na sessão ou por quanto tempo ela dura, e é exatamente aí que vivem esses quatro erros.

Tente você mesmo

Quatro cenários. Identifique os problemas e corrija cada um.

js
// 1. Cookie definido após o login
res.cookie('session', {
  sessionId: 'a3f9c2e7b418',
  userId: 4821,
  password: 'hunter2',
  ip: '203.0.113.42',
}, { maxAge: 1000000000000 })

// 2. Cookie definido após o login
res.cookie('sid', 'a3f9c2e7b418')

// 3. Uma sessão armazenada
{ sessionId: 'b7d1', userId: 91, role: 'admin', cart: [], theme: 'dark',
  ip: '203.0.113.42', expires: '3000-01-01T00:00:00Z' }

// 4. Um cookie e sua sessão
res.cookie('sid', { userId: 91, role: 'admin' }, { maxAge: 2000 })
{ sessionId: 'c4e2', userId: 91, role: 'admin', expires: '2099-01-01T00:00:00Z' }
Compare suas respostas
#ProblemasCorreção
1Dados sensíveis no cookie, e um maxAge de cerca de 32 anosManter apenas sessionId, maxAge: 1800000
2Nenhuma expiração, então muitos navegadores o mantêm indefinidamenteAdicionar maxAge
3Sobrecarregada, e expira no ano 3000Manter sessionId, userId, role, expiração em 30 minutos
4O cookie não tem id de sessão e carrega userId e role em vez disso; seu maxAge de 2 segundos é inutilizável; a sessão expira em 2099Colocar o id da sessão no cookie, descartar o resto, maxAge: 1800000, expiração em 30 minutos

O cenário 4 é o mais interessante, porque falha nas duas direções ao mesmo tempo. O cookie é ao mesmo tempo curto demais para ser útil e carrega dados que nunca deveriam sair do servidor, enquanto a sessão por trás dele dura quase um século.

Se suas correções diferirem nos detalhes, tudo bem. O que importa é chegar ao raciocínio.

Para onde isso vai

A identidade stateful é robusta porque o servidor continua no comando. Cada um desses quatro erros corrói isso: dados escapando do servidor, ou uma decisão que sobrevive além dos fatos que a sustentavam.

Tokens opacos (bearer tokens) começa o arco oposto, no qual o servidor deixa de manter qualquer sessão e o cliente passa a carregar sua própria prova.