- Las cookies HTTP son el mecanismo básico para mantener estado en la web, pero navegadores, servidores y bibliotecas estándar difieren en los caracteres permitidos y el manejo de errores, lo que puede terminar en fallas reales
- La familia RFC 6265 define condiciones distintas para los valores de
Set-Cookieque envía el servidor y los valores que aceptan los navegadores; los valores creados condocument.cookiechocan con las suposiciones de los parsers del lado servidor - Firefox, Chromium y Safari difieren en el tratamiento de espacios, comillas, comas, barras invertidas y Unicode; Safari, al encontrar caracteres prohibidos, no descarta toda la cookie sino que muestra un comportamiento de guardar solo la parte inicial
- Go puede omitir silenciosamente cookies JSON aceptadas por el navegador, Python
SimpleCookiepuede detener la carga después de una cookie que no entiende, y PHP, Ruby y Rust también tienen rangos permitidos distintos - Una sola cookie Unicode puede provocar errores 400/500 o fallas parciales en sitios importantes como Facebook, Netflix, Okta, WhatsApp, AWS y Apple Support, por lo que hace falta alinear con más claridad la especificación de cookies y el comportamiento de las bibliotecas
Cookies que el navegador acepta pero Go no puede leer
- Las cookies son datos definidos por
document.cookiede JavaScript o por un servidor HTTP, y se siguen incluyendo en las solicitudes HTTP que coinciden con su alcance hasta que vencen - El JavaScript de ejemplo guarda una cadena JSON tal cual como valor de una cookie de sesión
- El valor tiene la forma
{"ginger":"snap","peanutButter":"chocolate chip","snicker":"doodle"} - Al poner JSON en una cookie es común serializarlo en base64, pero el navegador define este valor sin problemas y lo envía en el encabezado
Cookie
- El valor tiene la forma
- El problema aparece cuando esta cookie se entrega a código que usa la biblioteca estándar de Go
- El parser de Go no puede interpretar esa cookie
- La falla se propaga en cadena hacia capas superiores del stack
Dos criterios dentro del RFC que no coinciden
- Las cookies se definieron a través de RFC 2109, RFC 2965 y RFC 6265, y existe una versión borrador actualmente en actualización
- El RFC trata los valores de las cookies de forma distinta en dos áreas
- La sección 4.1.1 excluye caracteres de control, espacios, comillas dobles, comas, punto y coma, barras invertidas, etc. de los valores que el servidor envía con
Set-Cookie - La sección 5.6 permite que el navegador acepte un rango mucho más amplio al parsear una cadena
Set-Cookie, salvo los caracteres de control
- La sección 4.1.1 excluye caracteres de control, espacios, comillas dobles, comas, punto y coma, barras invertidas, etc. de los valores que el servidor envía con
- El conflicto central es que los valores que debe enviar el servidor y los valores que debe aceptar el navegador no están alineados
- Si los navegadores solo aceptaran cookies definidas por el propio servidor, el impacto sería menor, pero
document.cookietambién puede crear cookies - El estándar no define con claridad si una biblioteca estándar que procesa encabezados
Cookiedebe ser permisiva como un agente de usuario o estricta como un servidor
- Si los navegadores solo aceptaran cookies definidas por el propio servidor, el impacto sería menor, pero
Diferencias entre navegadores sobre valores de cookies permitidos
-
Firefox
- La validación de valores de cookies de Firefox permite algunos caracteres que RFC 6265 prohíbe
- Los caracteres excluidos por recomendación del RFC que sí se permiten son los siguientes
0x09tabulación horizontal0x20espacio0x22comilla doble0x2Ccoma0x5Cbarra invertida
- Este comportamiento se incorporó en el pasado para mantener compatibilidad con Chrome y permanece en ambas bases de código
- La configuración
network.cookie.blockUnicodepuede rechazar valores0x80o superiores, y el trabajo relacionado se rastrea en bug 1797231 - El problema de permitir
0x7Fse corrigió en Firefox 108 mediante bug 1797235
-
Chromium
- Chromium rechaza en valores de cookies solo caracteres de control y punto y coma
- Es un poco más estricto que Firefox y no acepta
0x09tabulación horizontal - A diferencia del RFC, puede aceptar y reenviar espacios, comillas dobles, comas, barras invertidas y caracteres Unicode
-
Safari / WebKit
- El código de almacenamiento de cookies de Safari está dentro de
CFNetwork, de código cerrado, por lo que es difícil revisarlo directamente - Al definir mediante JavaScript valores de cookies de
0x00a0xFF, se comprobó que Safari permite los siguientes valores0x09tabulación horizontal0x20espacio0x22comilla doble0x5Cbarra invertida
- Safari no permite
0x7Fdelete ni caracteres0x80-FFhigh ASCII / Unicode - El RFC dice que, al encontrar un carácter de control, debe ignorarse toda la cookie, pero Safari acepta el valor hasta el punto anterior al carácter prohibido
- También se observó un bug de Safari en el que, al definir el valor
-- , --, elimina los espacios alrededor de la coma
- El código de almacenamiento de cookies de Safari está dentro de
Diferencias de parsing entre lenguajes y bibliotecas estándar
-
Go
- El código de cookies de Go se comporta relativamente cerca del texto del RFC sobre los valores que el servidor envía con
Set-Cookie - Permite espacios y comas, comunes en el uso real, pero no permite comillas dobles, punto y coma ni barras invertidas
- Si un encabezado
Cookiede ejemplo incluye una cookie JSON, el resultado derequest.Cookies()de Go solo conservacookie1=fooycookie3=bar cookie2, que el navegador acepta, se omite silenciosamente sin excepción ni error explícito
- El código de cookies de Go se comporta relativamente cerca del texto del RFC sobre los valores que el servidor envía con
-
PHP
- PHP no tiene una función nativa de parsing de cookies, por lo que es difícil afirmar con precisión el rango permitido, pero en pruebas el tratamiento de caracteres de control no fue consistente
- Valores como
0x00-0x09y0x0Dcarriage return funcionan - Al usar
0x10data link escape o0x7Fdelete, PHP devuelve un error 400 Bad Request - Las cookies Unicode también aparecen en la salida de las pruebas
-
Python
http.cookies.SimpleCookiede Python, al encontrar una cookie JSON, detiene silenciosamente la carga de las cookies posteriores- En el ejemplo de entrada, la salida conserva solo
cookie1=foo - Si un subdominio puede definir una cookie problemática en el dominio principal, una sola cookie puede romper el procesamiento de cookies de todo el sitio
- El manejo de caracteres de control también es irregular
- Algunos caracteres de control se cargan como valores vacíos
- Si se agrega
aaantes y después del valor, la cookie con carácter de control no se carga
-
Ruby
CGI::Cookie.parsede Ruby parece comportarse de forma muy permisiva al parsear- Acepta caracteres de control, tabulaciones, comillas dobles, comas, barras invertidas,
0x7Fy caracteres Unicode, y aplica percent-encoding al extraerlos del cookie jar - Este enfoque puede estar cerca de lo óptimo en el mundo de las cookies, pero el código que definió la cookie con
document.cookiequizá no espere un valor reflejado con percent-encoding
-
Rust
- Rust no ofrece funcionalidad básica de manejo de cookies, por lo que se revisó usando como referencia el popular crate
cookie - Con la configuración predeterminada, el crate
cookieestá entre los más permisivos y parece aceptar la cadena UTF-8 recibida
- Rust no ofrece funcionalidad básica de manejo de cookies, por lo que se revisó usando como referencia el popular crate
Impacto observado en sitios web reales
- El problema se descubrió durante la verificación manual de una actualización de una biblioteca de terceros en un sitio de prueba
- Era un cambio difícil de detectar con pruebas automatizadas
- Si se hubiera desplegado tal cual, los visitantes posteriores habrían recibido cookies rotas y podrían haber quedado bloqueados por errores desconocidos hasta revertir la actualización y eliminar las cookies
- Este problema no se limita a sitios pequeños ni a frameworks específicos
- Si desde la consola del navegador se define una cookie Unicode en el dominio como sigue, varios sitios importantes pueden romperse
document.cookie="unicodeCookie=🍪; domain=.grayduck.mn; Path=/; SameSite=Lax"
- Los casos observados fueron los siguientes
- Facebook: se muestra una página de error y también se rompen las imágenes
- Instagram y Threads: se produce un simple error 500
- Netflix: devuelve el error
NSES-500y también se rompe la página de ayuda - Okta: todas las páginas de inicio de sesión devuelven error 400
- WhatsApp: muestra “whatsapp error”
- Amazon: la mayor parte funciona, pero algunas funciones se rompen de forma aleatoria
- AWS: la consola de inicio de sesión devuelve error 400 y se interrumpe
- Apple Support: no puede cargar la lista de dispositivos
- Best Buy: la navegación funciona, pero la búsqueda no
- eBay: la mayor parte fue corregida, pero algunas partes siguen devolviendo error 400
- Home Depot: se corregirá próximamente
- Intuit: el único sitio que identificó la causa del error
- Outlook: aparece otro caso de error 400
La dificultad de corregir entre estándar y compatibilidad
- Corregir un problema en una especificación base de 30 años es muy difícil, y probablemente no haya una buena solución para este caso
- Tanto Mozilla como Google revisaron y trabajaron en la opción de bloquear estas cookies del lado del navegador
- Mozilla: bug 1797235, CVE-2023-5723, bug 1797231
- Google: bug 40061459
- Un bloqueo unilateral es complejo por problemas de compatibilidad
- Las cookies no ASCII son poco comunes, por debajo de 0.01% del total de cookies
- Hay telemetría que indica que aparecen con mucha más frecuencia en países como Argentina, México y Finlandia
- Mozilla implementó la configuración
network.cookie.blockUnicode, que puede activarse rápidamente, pero no la habilitó por problemas de compatibilidad de comportamiento con Chromium
- También podría ser posible corregirlo del lado servidor, pero eso abarca millones de sitios web y el manejo interno de errores de lenguajes y frameworks
- Lugares como Facebook o Netflix quizá puedan mitigarlo, pero es difícil que el operador promedio de un sitio tenga el tiempo o la capacidad para resolverlo
- La solución de fondo está en que el IETF HTTP Working Group alinee internamente la especificación de cookies y defina de forma estricta cómo deben comportarse los sistemas de manejo de cookies
- La aceptación o no de caracteres no ASCII debe ser igual del lado servidor y en los agentes de usuario
- Las etapas en que navegadores, lenguajes y frameworks procesan cookies también deberían ser explícitas, como en estándares modernos del W3C como Content Security Policy
- Que una sola cookie inválida interrumpa también el procesamiento de otras cookies puede causar todo tipo de fallas inesperadas, por lo que es difícil de aceptar
Procedimiento propuesto para procesar cookies
- Partir de
field-valuey dividir por;y,para crear una lista deraw-cookie-pair, sin tratar la coma como sinónimo del punto y coma - Procesar cada
raw-cookie-pairen este orden- Si no contiene
=, pasar al siguiente pair - Eliminar los espacios al principio y al final
- Tratar lo que está antes del primer
=comocookie-name-octetsy lo que está después comocookie-value-octets - Si el valor empieza con comilla doble, eliminar una comilla doble inicial y, si existe una comilla doble final, eliminar una
- Si el nombre o el valor tiene una forma que el servidor no puede aceptar, omitir ese pair
- Procesar la tupla restante
[cookie-name-octets, cookie-value-octets]de la forma definida por el servidor
- Si no contiene
- Se propone además que el servidor rechace las tuplas cuyo nombre de cookie no sea un token, y que rechace los valores de cookie que contengan octets que no estén en
cookie-octet
1 comentarios
Opiniones de Hacker News
Las cookies están llenas de trampas raras y comportamientos incómodos, pero el 99.95% del tiempo funcionan bien. Mi campo minado favorito de las cookies es el cookie shadowing: si configuras cookies con el mismo nombre pero variando solo atributos importantes como dominio y ruta, terminas con varias cookies casi iguales al mismo tiempo, y ni el backend ni JS tienen forma de distinguir cuál es cuál.
Puedes ir a https://example.com/somepath y escribir lo siguiente en la consola del navegador:
document.cookie = "foo=a";document.cookie = "foo=b; domain=.example.com";document.cookie = "foo=c; path=/somepath";document.cookieEn mi caso, el resultado fue
'foo=c; foo=a; foo=b'.Es un error realmente enorme.
/somepath, parece bastante razonable recibir C, el valor más específico de los tres. Como todos los valores se devuelven en orden, puedes conocer tanto el valor por ruta como el global, así que se siente como el mejor compromiso.Eso sí, no me gusta el mágico setter de
document.cookie, pero ya es algo de casi 30 años, así que no hay mucho que hacer.Este problema volvió a surgir hace poco cuando jshttp/cookie endureció la validación: https://github.com/jshttp/cookie/pull/167
Después de ese PR, la validación volvió a relajarse un poco, de forma parecida al código de navegador mencionado en el artículo.
El cambio original empezó en nuestro código, al encontrar un bug donde se armaba el header de cookies concatenando strings sin codificación. A veces el valor contenía espacios y rompía la solicitud; para evitarlo, queríamos sugerir a los desarrolladores usar
serialize()de jshttp/cookie, pero nos dimos cuenta de que la validación de esa función no era suficiente para detectar el bug que habíamos visto.Cuando propusimos una corrección, otra persona descubrió que la validación era tan laxa que permitía insertar JS en el campo de nombre de la cookie y hacer que en otra parte se interpretara como si fuera el valor. Terminó siendo una ruta de inyección de código bastante peculiar.
El artículo menciona el enfoque de Rust, pero a diferencia de otros lenguajes, la biblioteca estándar de Rust no incluye manejo de cookies. En realidad, se está viendo el comportamiento del crate de terceros
cookie, que también incluye una opción para hacer codificación porcentual, como Ruby: https://docs.rs/cookie/0.18.1/cookie/Dentro del protocolo HTTP parece haber, en la práctica, unos diez mil protocolos distintos incrustados. Los navegadores y los servidores web le fueron agregando todo tipo de funciones, cada una con su especificación y su especificación de facto, y todo eso se transporta bajo el paraguas casi universal de HTTP.
El cliente no puede indicar con cuál versión de esos diez mil no-estándares es compatible, y el servidor tampoco. La razón por la que no se puede actualizar la especificación es que el resto de los clientes no la entiende y tampoco hay compatibilidad hacia atrás.
Así que nos quedó un caos aleatorio en el que nadie puede ponerse de acuerdo ni arreglar nada. Como tampoco hay obsolescencia planificada, hay que seguir cargando con las malas decisiones del pasado.
Hace unos 10 años implementé sesiones basadas en cookies en un proyecto, y me costó muchísimo depurar por qué la autenticación funcionaba en Safari pero no en Chrome. No recuerdo exactamente cuál era cuál, pero uno de los navegadores simplemente no configuraba la cookie si el formato no era correcto.
No era que estuviéramos haciendo algo especialmente raro; si mal no recuerdo, era algo como la diferencia entre
-y_.Set-Cookie.Alguna vez no pude usar
camelCaseen claves de cookies por este problema.Buscando, no logro encontrar el issue exacto.
Me parece que, desde poco después de que se introdujeron las cookies, se consideró que el uso razonable era guardar solo tokens opacos, para que el servidor reconociera al mismo cliente la próxima vez, y almacenar todo lo demás del lado del servidor.
No veo por qué es un problema que el cliente, en principio, pueda manejar valores que el servidor jamás enviaría. Simplemente no envíes esos valores, y no tienes que preocuparte por acertijos como “¿qué pasaría si los enviara?”.
Aun así, como son el único lugar para guardar tokens opacos, hay que usarlas para autenticación.
El parseo del header de cookies es un desastre. El “estándar” no refleja el comportamiento que existe en la práctica, cada servidor backend, librería y framework acepta formatos distintos, y los navegadores hacen otra cosa más.
Si controlas por completo el frontend y el backend, no es un gran problema, pero en cuanto tienes que integrar cosas distintas, la situación se vuelve absurda muy rápido.
Las cookies parecen un caos grande y complejo, y al mismo tiempo son casi imposibles de cambiar por la compatibilidad hacia atrás. En estos casos, quizá lo correcto sería crear un mecanismo nuevo, totalmente separado.
Por ejemplo, se podría especificar un mecanismo como NewCookie y rediseñarlo desde cero para que se comporte de forma consistente. Podría traer medidas de seguridad modernas integradas, una especificación más estricta y soporte adecuado para Unicode.
Set-Cookie2, que fue descartado: https://stackoverflow.com/q/9462180/3474615Al menos para algunos casos de uso; claro, no está integrado directamente con los headers.
Como las cookies ya existen, estamos atados a ellas.
Pasé un mes entero persiguiendo un problema en el que iOS Safari se comía arbitrariamente las cookies de dominios controlados por nuestros clientes. Nunca he visto que el estado de sesión desaparezca así en dominios como Google, Twitter o Facebook.
Hablando un poco más en serio, convendría evitar la palabra cookie y ponerle un nombre totalmente distinto. La palabra cookie carga con demasiado equipaje.
El autor empezó metiendo el resultado de
JSON.stringifyen una cookie, y me sorprendió que el problema no fuera que alguien hubiera puesto un punto y coma dentro del JSON convertido a string.Creo que la mayoría de los dolores de cabeza alrededor de las cookies aparecen cuando intentas meter input arbitrario del usuario en una cookie. No hay que hacer eso. Si usas solo cadenas ASCII alfanuméricas de longitud fija, como con los tokens de autenticación, todo va bien.
Coincido en que es todo un campo minado.
Como desarrollador, la forma de esquivarlo es codificar los valores en Base64 seguro para URL. Así obtienes bytes crudos y puedes usar la representación interna que quieras. Eso sí, como dice el artículo, no es algo que puedas controlar al 100%. Es un agente de usuario, así que también tiene sentido que sea así.
Ojalá más agentes de usuario eligieran el cumplimiento del estándar en lugar de “bytes en el cable y rezar”. Las respuestas 400 de las capturas son respuestas acordes a la especificación. Habría sido mejor que los headers hubieran sido UTF-8 desde el principio, o que primero fueran ASCII y luego se permitiera UTF-8. Aunque lo primero es difícil por causalidad, y lo segundo también podría causar problemas porque vuelve legales valores que antes eran ilegales.
base64urlno es compatible conbase64más codificación URL en alrededor del 3% de los casos; durante el desarrollo se pasa por alto fácilmente, pero en producción seguro explota.=,/y+, así que también se puede usar la codificación Base64 estándar :)El artículo se burla de la ley de Postel, pero si quien establece la cookie hubiera sido conservador al enviar, este artículo no habría hecho falta en primer lugar.
A veces esas minas no son simples bugs, sino enormes agujeros de seguridad.
Si el cliente envía datos que no cumplen la especificación, eso es un bug y hay que corregirlo. Nunca debería darse por sentado que el servidor adivine la intención y los acepte.