3 pontos por GN⁺ 2024-11-22 | 1 comentários | Compartilhar no WhatsApp
  • 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-Cookie e os valores aceitos pelo navegador, e valores criados com document.cookie entram 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 SimpleCookie do 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.cookie no 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 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
  • 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.cookie também pode criar cookies
    • A especificação não deixa claro se bibliotecas padrão que processam o cabeçalho Cookie devem ser tolerantes como um user agent ou rígidas como um servidor

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
      • 0x09 horizontal tab
      • 0x20 espaço
      • 0x22 aspas duplas
      • 0x2C vírgula
      • 0x5C barra 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.blockUnicode pode rejeitar valores 0x80 ou superiores, e o trabalho relacionado é acompanhado no bug 1797231
    • A aceitação indevida de 0x7F foi 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 0x09 horizontal 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 0x00 a 0xFF, observou-se que o Safari aceita
      • 0x09 horizontal tab
      • 0x20 espaço
      • 0x22 aspas duplas
      • 0x5C barra invertida
    • O Safari não aceita 0x7F delete nem caracteres 0x80-FF high 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

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 Cookie do exemplo inclui um cookie JSON, o resultado de request.Cookies() no Go mantém apenas cookie1=foo e cookie3=bar
    • O cookie2, que o navegador aceita, some silenciosamente sem exceção nem erro explícito
  • 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-0x09 e 0x0D carriage return funcionam
    • Com 0x10 data link escape ou 0x7F delete, o PHP retorna erro 400 Bad Request
    • Cookies Unicode também aparecem na saída de teste
  • Python

    • O http.cookies.SimpleCookie do 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 aa for adicionado antes e depois do valor, o cookie com caractere de controle deixa de ser carregado
  • Ruby

    • O CGI::Cookie.parse do Ruby parece ser bem permissivo ao fazer parse
    • Aceita caracteres de controle, tab, aspas duplas, vírgula, barra invertida, 0x7F e 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.cookie pode não esperar um valor refletido em percent-encoding
  • 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 cookie parece estar entre os mais permissivos, aceitando a string UTF-8 recebida

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-500 e 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
  • 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-value e dividir em uma lista de raw-cookie-pair por ; e ,, sem tratar vírgula como sinônimo de ponto e vírgula
  • Cada raw-cookie-pair deve 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 como cookie-name-octets, e depois dele como cookie-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
  • 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

 
GN⁺ 2024-11-22
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.cookie
    No meu caso, o resultado foi 'foo=c; foo=a; foo=b'

    • Não sei quem projetou isso na empresa, mas colocaram os ambientes de staging e desenvolvimento no mesmo domínio, e a empresa inteira, enorme, está seguindo esse padrão
      É um erro realmente gigantesco
    • Fico pensando que isso talvez explique boa parte dos comportamentos estranhos que aparecem quando se usam várias contas no mesmo site, no mesmo navegador
    • Se você está em /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-termo
      Dito 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 fazer
    • A propósito, tecnicamente o ponto no início do domínio não é permitido e é ignorado: https://www.rfc-editor.org/rfc/rfc6265#section-4.1.2.3
      Esse 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 vimos
      Quando 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
    • Sim, há mesmo muitos riscos. Este material aborda esse problema e dores de cabeça relacionadas em detalhes: https://www.usenix.org/conference/usenixsecurity15/technical-sessions/presentation/zheng
  • 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/

    • É o modo de virar padrão de facto ao ocupar cedo um bom nome
  • 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

    • Parte da culpa também é de equipamentos de middleware horríveis que bloqueiam protocolos que não entendem. É aquela ideia de que “é mais seguro fazer falhar por padrão”, então, para sempre, todo novo tráfego de aplicação terá de ser tunelado sobre HTTP se quiser funcionar na internet real
    • Para ser sincero, hoje já fiz as pazes com esse mundo, e talvez eu nem goste mais de um mundo com descontinuação planejada do que dele
    • Se não quisermos que uma empresa monopolista defina uma especificação limpa e imponha descontinuações à vontade, o preço a pagar é aceitar a anarquia
  • 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 _

    • Acho que havia alguma diferença de sensibilidade a maiúsculas e minúsculas entre Safari e Chrome. Talvez fosse o cabeçalho Set-Cookie
      Já deixei de usar camelCase em chaves de cookie por causa desse problema no passado
      Procurando, 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?”

    • Cookies são uma tecnologia antiga. Foram uma das primeiras coisas introduzidas nos anos 90, quando a web ainda era jovem, e algumas ideias ruins se repetiram várias vezes
      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

    • É curioso mencionarem NewCookie, porque na prática já existiu um cabeçalho Set-Cookie2 que foi abandonado: https://stackoverflow.com/q/9462180/3474615
    • NewCookie corresponderia mais ou menos ao Local Storage dos navegadores
      Pelo menos para alguns casos de uso; claro, ele não se integra diretamente aos cabeçalhos
    • O problema central parece ser que cookies estão profundamente ligados a rastreamento. Se alguém tentar criar cookies melhores hoje, provavelmente será barrado por defensores da privacidade que não querem que esse conceito exista de forma alguma
      Como os cookies já existem, estamos presos a eles
    • O lugar mais seguro para armazenar estado do lado do cliente é o DOM e a URL. Isso não cobre todos os casos de uso, mas cobre coisas como links de pré-aprovação em e-mails
      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
    • O nome deveria ser melhor que NewCookie. Dá para sugerir coisas como SuperCookie, UltraCookie ou BetterCookie
      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.stringify em um cookie, e foi até surpreendente que o problema não tenha sido alguém colocar um ponto e vírgula dentro do JSON serializado
    A 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

    • Ao falar em Base64 seguro para URL, é preciso especificar exatamente o que isso significa. A codificação base64url não é compatível com base64 mais URL encoding em cerca de 3% dos casos; é algo fácil de deixar passar durante o desenvolvimento, mas que inevitavelmente explode em produção
    • Valores de cookies podem conter os caracteres =, / 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

    • Merece ser zombada. A Lei de Postel foi uma ideia terrível e criou campos minados por toda parte
      À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
    • O problema da Lei de Postel é justamente que o emissor nunca é conservador. Os detalhes de comportamento que a maioria dos receptores aceita acabam sendo usados pelo emissor