Lidar com cookies é um campo minado
(grayduck.mn)- Cookies HTTP são o mecanismo básico para manter estado na web, mas navegadores, servidores e bibliotecas padrão divergem nos caracteres permitidos e no tratamento de erros, o que pode levar a falhas reais
- A família de RFC 6265 trata de forma diferente os valores enviados pelo servidor em
Set-Cookiee os valores aceitos pelo navegador, e valores criados comdocument.cookieentram em conflito com pressupostos de parsers no lado do servidor - Firefox, Chromium e Safari se comportam de forma diferente com espaços, aspas, vírgulas, barras invertidas e Unicode, e o Safari mostra um comportamento em que, ao encontrar um caractere proibido, armazena apenas a parte inicial do cookie em vez do cookie inteiro
- O Go pode omitir silenciosamente cookies JSON que o navegador aceitou, o
SimpleCookiedo Python pode interromper o carregamento após um cookie que ele não entende, e PHP, Ruby e Rust também têm faixas de aceitação diferentes - Um único cookie Unicode pode causar erros 400/500 ou falhas parciais em grandes sites como Facebook, Netflix, Okta, WhatsApp, AWS e Apple Support, então é preciso alinhar melhor a especificação de cookies com o comportamento das bibliotecas
O navegador aceita, mas o Go não consegue ler o cookie
- Cookies são dados definidos por
document.cookieno JavaScript ou por um servidor HTTP, e continuam sendo incluídos nas requisições HTTP compatíveis com o escopo até expirarem - O exemplo em JavaScript salva uma string JSON diretamente como valor de um cookie de sessão
- O valor tem a forma
{"ginger":"snap","peanutButter":"chocolate chip","snicker":"doodle"} - Ao colocar JSON em cookies, muita gente usa serialização em base64, mas o navegador define esse valor sem problemas e o envia no cabeçalho
Cookie
- O valor tem a forma
- O problema aparece quando esse cookie chega a um código que usa a biblioteca padrão do Go
- O parser do Go não consegue interpretar esse cookie
- A falha se propaga em cadeia para camadas superiores da stack
Dois critérios dentro da RFC não se alinham
- Cookies foram definidos ao longo de RFC 2109, RFC 2965 e RFC 6265, e há uma draft version em atualização no momento
- A RFC trata o valor de cookies de forma diferente em dois contextos
- A Section 4.1.1 exclui caracteres de controle, espaços, aspas duplas, vírgulas, ponto e vírgula, barras invertidas etc. dos valores enviados pelo servidor em
Set-Cookie - A Section 5.6 permite que o navegador aceite uma faixa muito mais ampla ao fazer parse da string
Set-Cookie, exceto por caracteres de controle
- A Section 4.1.1 exclui caracteres de controle, espaços, aspas duplas, vírgulas, ponto e vírgula, barras invertidas etc. dos valores enviados pelo servidor em
- O conflito central é que o valor que o servidor deve enviar e o valor que o navegador deve aceitar não estão alinhados
- Se o navegador só aceitasse cookies definidos pelo próprio servidor, o impacto seria menor, mas
document.cookietambém pode criar cookies - A especificação não deixa claro se bibliotecas padrão que processam o cabeçalho
Cookiedevem ser tolerantes como um user agent ou rígidas como um servidor
- Se o navegador só aceitasse cookies definidos pelo próprio servidor, o impacto seria menor, mas
Diferenças entre navegadores no que aceitam como valor de cookie
-
Firefox
- A validação de valor de cookie no Firefox permite alguns caracteres que a RFC 6265 proíbe
- Os caracteres recomendados para exclusão pela RFC que ainda são aceitos incluem
0x09horizontal tab0x20espaço0x22aspas duplas0x2Cvírgula0x5Cbarra invertida
- Esse comportamento foi introduzido historicamente para manter compatibilidade com o Chrome e continua presente nas duas bases de código
- A configuração
network.cookie.blockUnicodepode rejeitar valores0x80ou superiores, e o trabalho relacionado é acompanhado no bug 1797231 - A aceitação indevida de
0x7Ffoi corrigida no Firefox 108 no bug 1797235
-
Chromium
- O Chromium rejeita apenas caracteres de controle e ponto e vírgula em valores de cookie
- Ele é um pouco mais rígido que o Firefox e não aceita
0x09horizontal tab - Ao contrário da RFC, aceita e reenvia espaço, aspas duplas, vírgula, barra invertida e caracteres Unicode
-
Safari / WebKit
- O código de armazenamento de cookies no Safari fica dentro do
CFNetwork, que é fechado, então é difícil confirmar diretamente o comportamento - Testando com JavaScript valores de cookie de
0x00a0xFF, observou-se que o Safari aceita0x09horizontal tab0x20espaço0x22aspas duplas0x5Cbarra invertida
- O Safari não aceita
0x7Fdelete nem caracteres0x80-FFhigh ASCII / Unicode - A RFC diz que, ao encontrar um caractere de controle, o cookie inteiro deve ser ignorado, mas o Safari aceita o valor até o ponto anterior ao caractere proibido
- Também foi observado um bug no Safari em que, ao definir
-- , --, ele remove os espaços ao redor da vírgula
- O código de armazenamento de cookies no Safari fica dentro do
Diferenças de parsing entre linguagens e bibliotecas padrão
-
Go
- O código de cookies do Go segue relativamente de perto a redação da RFC para valores enviados pelo servidor em
Set-Cookie - Na prática, aceita espaço e vírgula, que são comuns, mas não aceita aspas duplas, ponto e vírgula nem barra invertida
- Quando o cabeçalho
Cookiedo exemplo inclui um cookie JSON, o resultado derequest.Cookies()no Go mantém apenascookie1=fooecookie3=bar - O
cookie2, que o navegador aceita, some silenciosamente sem exceção nem erro explícito
- O código de cookies do Go segue relativamente de perto a redação da RFC para valores enviados pelo servidor em
-
PHP
- O PHP não tem uma função nativa específica para parsing de cookies, então é difícil afirmar com precisão a faixa aceita, mas os testes mostram comportamento inconsistente com caracteres de controle
- Valores como
0x00-0x09e0x0Dcarriage return funcionam - Com
0x10data link escape ou0x7Fdelete, o PHP retorna erro 400 Bad Request - Cookies Unicode também aparecem na saída de teste
-
Python
- O
http.cookies.SimpleCookiedo Python interrompe silenciosamente o carregamento dos cookies seguintes ao encontrar um cookie JSON - Na entrada de exemplo, a saída mantém apenas
cookie1=foo - Se um subdomínio puder definir um cookie problemático para o domínio base, um único cookie pode quebrar o processamento de cookies do site inteiro
- O tratamento de caracteres de controle também é irregular
- Alguns caracteres de controle são carregados como valor vazio
- Se
aafor adicionado antes e depois do valor, o cookie com caractere de controle deixa de ser carregado
- O
-
Ruby
- O
CGI::Cookie.parsedo Ruby parece ser bem permissivo ao fazer parse - Aceita caracteres de controle, tab, aspas duplas, vírgula, barra invertida,
0x7Fe Unicode, e aplica percent-encoding ao recuperar do cookie jar - Essa abordagem talvez seja uma das melhores possíveis no mundo dos cookies, mas o código que definiu o cookie com
document.cookiepode não esperar um valor refletido em percent-encoding
- O
-
Rust
- Rust não oferece tratamento de cookies por padrão, então a referência usada foi o crate popular
cookie - Na configuração padrão, o crate
cookieparece estar entre os mais permissivos, aceitando a string UTF-8 recebida
- Rust não oferece tratamento de cookies por padrão, então a referência usada foi o crate popular
Impacto observado em sites reais
- O problema foi descoberto durante a validação manual de uma atualização de biblioteca third-party em um site de teste
- Era uma mudança difícil de capturar com testes automatizados
- Se tivesse ido para produção, visitantes posteriores poderiam receber um cookie quebrado e ficar presos em erros difíceis de diagnosticar até haver rollback e limpeza dos cookies
- O problema não se limita a sites pequenos nem a um framework específico
- Ao definir no console do navegador um cookie Unicode como abaixo para o domínio, vários sites importantes podem quebrar
document.cookie="unicodeCookie=🍪; domain=.grayduck.mn; Path=/; SameSite=Lax"
- Casos observados incluem
- Facebook: aparece uma página de erro e até as imagens quebram
- Instagram e Threads: ocorre um erro 500 simples
- Netflix: retorna erro
NSES-500e até a página de ajuda quebra - Okta: todas as páginas de login retornam erro 400
- WhatsApp: aparece “whatsapp error”
- Amazon: a maior parte funciona, mas alguns recursos quebram aleatoriamente
- AWS: o console de login retorna erro 400 e para de funcionar
- Apple Support: não consegue carregar a lista de dispositivos
- Best Buy: a navegação funciona, mas a busca não
- eBay: em grande parte já foi corrigido, mas alguns pontos ainda retornam erro 400
- Home Depot: correção prevista
- Intuit: foi o único site a identificar a causa do erro
- Outlook: aparece mais um caso de erro 400
A dificuldade de corrigir entre especificação e compatibilidade
- É muito difícil corrigir um problema numa especificação-base de 30 anos, e talvez não exista uma solução realmente boa para este caso
- Tanto Mozilla quanto Google estudaram e trabalharam em formas de bloquear esse tipo de cookie no lado do navegador
- Mozilla: bug 1797235, CVE-2023-5723, bug 1797231
- Google: bug 40061459
- Um bloqueio unilateral é complicado por causa da compatibilidade
- Cookies não ASCII são raros, menos de 0,01% do total
- Há telemetria mostrando que eles aparecem com muito mais frequência em países como Argentina, México e Finlândia
- A Mozilla implementou a configuração
network.cookie.blockUnicode, que pode ser ativada rapidamente, mas não a habilita por causa de problemas de compatibilidade comportamental com o Chromium
- Correções no lado do servidor também podem ajudar, mas o problema atravessa milhões de sites, linguagens e o tratamento interno de erros em frameworks
- Lugares como Facebook ou Netflix talvez consigam mitigar, mas é difícil esperar que o operador médio de um site tenha tempo ou capacidade para resolver isso
- A solução de fundo é que o IETF HTTP Working Group alinhe internamente a especificação de cookies e defina com rigidez como os sistemas de tratamento de cookies devem funcionar
- A aceitação ou não de caracteres não ASCII deve ser igual no lado do servidor e no user agent
- As etapas de tratamento de cookies em navegadores, linguagens e frameworks também deveriam ser explícitas, como em padrões W3C modernos como Content Security Policy
- Um comportamento em que um único cookie inválido interrompe o tratamento dos demais é difícil de aceitar, porque pode levar a várias falhas inesperadas
Procedimento proposto para tratamento de cookies
- Começar de
field-valuee dividir em uma lista deraw-cookie-pairpor;e,, sem tratar vírgula como sinônimo de ponto e vírgula - Cada
raw-cookie-pairdeve ser processado nesta ordem- Se não houver
=, passar para o próximo pair - Remover espaços em branco do início e do fim
- Antes do primeiro
=tratar comocookie-name-octets, e depois dele comocookie-value-octets - Se o valor começar com aspas duplas, remover uma aspas dupla inicial e, se houver uma aspas dupla final, removê-la também
- Se o nome ou o valor estiverem em uma forma que o servidor não possa aceitar, ignorar esse pair
- O tuple restante
[cookie-name-octets, cookie-value-octets]deve ser tratado conforme a definição do servidor
- Se não houver
- Como etapa adicional, propõe-se que o servidor rejeite tuples em que o nome do cookie não seja um token e rejeite valores que incluam octets fora de
cookie-octet
1 comentários
Comentários no Hacker News
Cookies são cheios de armadilhas estranhas e comportamentos incômodos, mas 99,95% funcionam bem. Meu campo minado favorito em cookies é o cookie shadowing, em que, se você definir cookies com o mesmo nome, mudando apenas atributos importantes como domínio e caminho, vários cookies quase iguais passam a existir ao mesmo tempo, e nem o backend nem o JS têm como distinguir qual é qual
Basta ir a https://example.com/somepath e digitar o seguinte no console do navegador
document.cookie = "foo=a";document.cookie = "foo=b; domain=.example.com";document.cookie = "foo=c; path=/somepath";document.cookieNo meu caso, o resultado foi
'foo=c; foo=a; foo=b'É um erro realmente gigantesco
/somepath, parece bem razoável receber C, o valor mais específico entre os três. Como todos os valores são retornados em ordem, dá para saber tanto o valor por caminho quanto o valor global, então parece o melhor meio-termoDito isso, não gosto do setter mágico de
document.cookie, mas é algo que já tem quase 30 anos, então não há muito o que fazerEsse problema voltou à tona recentemente quando a validação foi reforçada em jshttp/cookie: https://github.com/jshttp/cookie/pull/167
Depois desse PR, a validação foi novamente afrouxada um pouco, de forma parecida com o código de navegador mencionado no artigo
A mudança original começou no nosso código, ao encontrarmos um bug em que se montava o cabeçalho de cookie concatenando strings sem encoding. Às vezes entrava um espaço no valor e a requisição quebrava; para evitar isso, queríamos sugerir aos desenvolvedores que usassem
serialize()do jshttp/cookie, mas percebemos que a validação dessa função não era suficiente para pegar o bug que vimosQuando propusemos a correção, outra pessoa percebeu que a validação era permissiva demais, permitindo inserir JS no campo de nome do cookie e fazer com que, em outro lugar, aquilo fosse interpretado como valor. Virava um caminho bem peculiar de injeção de código
O artigo menciona a abordagem em Rust, mas, diferentemente de outras linguagens, a biblioteca padrão do Rust não inclui funcionalidade de tratamento de cookies. Na prática, o que se está observando é o comportamento do crate de terceiros
cookie, que também inclui uma opção para fazer percent-encoding, como no Ruby: https://docs.rs/cookie/0.18.1/cookie/Parece que dentro do protocolo HTTP há, na prática, umas dez mil protocolos diferentes embutidos. Navegadores e servidores web acrescentaram todo tipo de funcionalidade, cada uma com sua especificação e sua especificação de facto, e tudo isso é transmitido sob o guarda-chuva quase único e genérico do HTTP
O cliente não consegue especificar com qual versão dessas dez mil coisas não especificadas é compatível, e o servidor também não. O motivo pelo qual não se pode atualizar a especificação é que os demais clientes não entenderiam, e não há compatibilidade retroativa
Então sobra um caos aleatório com o qual ninguém consegue concordar nem consertar. Como não há descontinuação planejada, decisões ruins do passado precisam continuar sendo arrastadas
Em um projeto de cerca de 10 anos atrás, implementei sessões baseadas em cookies e sofri muito depurando por que a autenticação funcionava no Safari, mas não no Chrome. Não lembro exatamente qual era qual, mas um dos navegadores simplesmente não definia o cookie quando o formato não batia
Eu não estava fazendo nada particularmente estranho; pelo que lembro, acho que era uma diferença entre
-e_Set-CookieJá deixei de usar
camelCaseem chaves de cookie por causa desse problema no passadoProcurando, não consigo encontrar o issue exato
Desde pouco depois da introdução dos cookies, acho que o uso razoável era considerado colocar apenas tokens opacos, para que o servidor reconhecesse que era o mesmo cliente na próxima vez, e guardar todo o restante no lado do servidor
Não entendo por que seria um problema o cliente conseguir, em princípio, processar valores que o servidor jamais enviaria. Basta não enviar esses valores, sem precisar se preocupar com enigmas do tipo “o que aconteceria se eu enviasse isso?”
Ainda assim, como são o único lugar para armazenar um token opaco, precisam ser usados para autenticação
A análise de cabeçalhos de cookies é uma bagunça. O “padrão” não reflete o comportamento que existe no mundo real; servidores backend, bibliotecas e frameworks aceitam formatos diferentes, e os navegadores fazem outra coisa ainda
Se você controla totalmente o frontend e o backend, isso não é um grande problema, mas no momento em que precisa integrar coisas diferentes, a situação fica idiota muito rápido
Cookies parecem uma grande e complexa zona de caos e, ao mesmo tempo, são quase impossíveis de mudar por causa da compatibilidade retroativa. Em casos assim, talvez o certo seja criar um mecanismo novo, completamente separado
Por exemplo, poderia ser especificado um novo mecanismo como NewCookie, redesenhado desde o início para se comportar de forma consistente. Poderia trazer medidas modernas de segurança embutidas, uma especificação mais rigorosa e suporte adequado a Unicode
Set-Cookie2que foi abandonado: https://stackoverflow.com/q/9462180/3474615Pelo menos para alguns casos de uso; claro, ele não se integra diretamente aos cabeçalhos
Como os cookies já existem, estamos presos a eles
Passei um mês inteiro investigando o iOS Safari devorando arbitrariamente cookies de domínios controlados por clientes. Nunca vi estado de sessão desaparecer assim em domínios como Google, Twitter ou Facebook
Falando um pouco mais a sério, seria melhor evitar a palavra cookie e dar um nome totalmente diferente. A palavra cookie carrega bagagem demais
O autor começou colocando o resultado de
JSON.stringifyem um cookie, e foi até surpreendente que o problema não tenha sido alguém colocar um ponto e vírgula dentro do JSON serializadoA maior parte das dores de cabeça em torno de cookies parece surgir quando se tenta colocar entrada arbitrária de usuário em cookies. Não faça isso. Se você usar apenas strings ASCII alfanuméricas de tamanho fixo, como em tokens de autenticação, fica tudo bem
Concordo que é uma baita área minada
Como desenvolvedor, a forma de contornar é codificar os valores em Base64 seguro para URL. Assim você obtém bytes brutos e pode usar a representação interna que quiser. Mas, como o texto diz, não há como controlar isso 100%. É um user agent, então tem que ser assim também
Eu gostaria que mais user agents escolhessem a conformidade com padrões em vez de “bytes no fio e fé”. As respostas 400 nos screenshots estão de acordo com a especificação. Teria sido melhor se os cabeçalhos fossem UTF-8 desde o início, ou se começassem como ASCII e depois passassem a permitir UTF-8. A primeira opção, porém, é difícil por uma questão de causalidade, e a segunda ainda poderia causar problemas por tornar legais valores que antes eram ilegais
base64urlnão é compatível combase64mais URL encoding em cerca de 3% dos casos; é algo fácil de deixar passar durante o desenvolvimento, mas que inevitavelmente explode em produção=,/e+, então também dá para usar codificação Base64 padrão :)O texto zomba da Lei de Postel, mas, se quem define o cookie tivesse sido conservador ao enviar, este texto nem teria sido necessário
Às vezes, essas minas não são apenas bugs simples, mas grandes brechas de segurança
Se um cliente envia dados que não seguem a especificação, isso é um bug e deve ser corrigido. Jamais deveria se tornar normal o servidor adivinhar a intenção e aceitar