- Les cookies HTTP sont le mécanisme de base qui permet au Web de conserver un état, mais les navigateurs, serveurs et bibliothèques standard divergent sur les caractères autorisés et la gestion des erreurs, au point de provoquer de vraies pannes
- La famille de RFC 6265 applique des règles différentes entre les valeurs
Set-Cookieenvoyées par le serveur et celles acceptées par le navigateur, et les valeurs créées viadocument.cookieentrent en conflit avec les hypothèses des parseurs côté serveur - Firefox, Chromium et Safari se comportent différemment sur les espaces, guillemets, virgules, barres obliques inverses et caractères Unicode, et Safari montre un comportement où, face à un caractère interdit, il ne conserve que le début du cookie au lieu de rejeter l’ensemble
- Go peut omettre silencieusement des cookies JSON pourtant acceptés par le navigateur, Python
SimpleCookiepeut arrêter le chargement après un cookie qu’il ne comprend pas, et PHP, Ruby et Rust ont eux aussi des plages d’acceptation très différentes - Un seul cookie Unicode peut provoquer des erreurs 400/500 ou des pannes partielles sur de grands sites comme Facebook, Netflix, Okta, WhatsApp, AWS ou Apple Support, d’où la nécessité d’aligner plus clairement la spécification des cookies et le comportement des bibliothèques
Des cookies que le navigateur accepte, mais que Go ne sait pas lire
- Les cookies sont des données définies soit par
document.cookieen JavaScript, soit par un serveur HTTP, et ils continuent d’être inclus dans les requêtes HTTP correspondant à leur périmètre jusqu’à leur expiration - L’exemple JavaScript enregistre une chaîne JSON telle quelle comme valeur d’un cookie de session
- La valeur a la forme
{"ginger":"snap","peanutButter":"chocolate chip","snicker":"doodle"} - Lorsqu’on met du JSON dans un cookie, on le sérialise souvent en base64, mais le navigateur accepte ici cette valeur sans problème et l’envoie dans l’en-tête
Cookie
- La valeur a la forme
- Le problème apparaît quand ce cookie est transmis à du code utilisant la bibliothèque standard Go
- Le parseur Go ne parvient pas à interpréter ce cookie
- L’échec se propage en cascade vers les niveaux supérieurs de la pile
Deux critères contradictoires dans la RFC
- Les cookies ont été définis au fil de RFC 2109, RFC 2965 puis RFC 6265, et une version de travail est en cours de mise à jour
- La RFC traite différemment la valeur d’un cookie dans deux contextes
- La section 4.1.1 exclut, pour les valeurs envoyées par le serveur via
Set-Cookie, les caractères de contrôle, les espaces, les guillemets doubles, les virgules, les points-virgules, les barres obliques inverses, etc. - La section 5.6 demande au navigateur, lors du parsing d’une chaîne
Set-Cookie, d’accepter une plage bien plus large, hormis les caractères de contrôle
- La section 4.1.1 exclut, pour les valeurs envoyées par le serveur via
- Le conflit central est que les valeurs qu’un serveur doit envoyer et celles qu’un navigateur doit accepter ne sont pas alignées
- Si le navigateur n’acceptait que les cookies posés par le serveur lui-même, l’impact serait limité, mais
document.cookiepeut aussi créer des cookies - La norme ne dit pas clairement si les bibliothèques standard qui traitent l’en-tête
Cookiedoivent être aussi tolérantes qu’un user agent ou aussi strictes qu’un serveur
- Si le navigateur n’acceptait que les cookies posés par le serveur lui-même, l’impact serait limité, mais
Différences entre navigateurs sur les valeurs de cookie acceptées
-
Firefox
- Le contrôle des valeurs de cookie dans Firefox autorise certains caractères que RFC 6265 interdit
- Parmi les caractères que la RFC recommande d’exclure mais que Firefox accepte, on trouve
0x09horizontal tab0x20espace0x22guillemet double0x2Cvirgule0x5Cbarre oblique inverse
- Ce comportement a été introduit historiquement pour rester compatible avec Chrome, et il subsiste dans les deux bases de code
- Le paramètre
network.cookie.blockUnicodepeut rejeter les valeurs0x80et au-delà, et le suivi se fait dans bug 1797231 - Le problème d’acceptation de
0x7Fest corrigé dans Firefox 108 via bug 1797235
-
Chromium
- Chromium rejette uniquement les caractères de contrôle et le point-virgule dans les valeurs de cookie
- Il est légèrement plus strict que Firefox et n’accepte pas
0x09horizontal tab - Contrairement à la RFC, il accepte puis renvoie les espaces, guillemets doubles, virgules, barres obliques inverses et caractères Unicode
-
Safari / WebKit
- Le code de stockage des cookies dans Safari se trouve dans
CFNetwork, en source fermée, ce qui rend sa vérification directe difficile - Des tests en JavaScript en définissant des valeurs de cookie de
0x00à0xFFmontrent que Safari accepte0x09horizontal tab0x20espace0x22guillemet double0x5Cbarre oblique inverse
- Safari n’accepte ni
0x7Fdelete ni les caractères0x80-FFhigh ASCII / Unicode - La RFC dit que tout le cookie doit être ignoré lorsqu’un caractère de contrôle est rencontré, mais Safari accepte la valeur jusqu’au caractère interdit
- Un bug Safari a aussi été observé : en définissant
-- , --, les espaces autour de la virgule sont supprimés
- Le code de stockage des cookies dans Safari se trouve dans
Différences de parsing selon les langages et bibliothèques standard
-
Go
- Le code des cookies de Go suit d’assez près le texte de la RFC pour les valeurs qu’un serveur envoie via
Set-Cookie - Il autorise les espaces et virgules, courants en pratique, mais pas les guillemets doubles, points-virgules ni barres obliques inverses
- Si l’en-tête
Cookied’exemple contient un cookie JSON, le résultat derequest.Cookies()dans Go ne conserve quecookie1=fooetcookie3=bar - Le
cookie2accepté par le navigateur disparaît silencieusement, sans exception ni erreur explicite
- Le code des cookies de Go suit d’assez près le texte de la RFC pour les valeurs qu’un serveur envoie via
-
PHP
- PHP n’a pas de fonction native dédiée au parsing des cookies, ce qui rend difficile la définition exacte de sa plage d’acceptation, mais les tests montrent un comportement incohérent sur les caractères de contrôle
- Des valeurs comme
0x00-0x09et0x0Dcarriage return fonctionnent - Avec
0x10data link escape ou0x7Fdelete, PHP renvoie une erreur 400 Bad Request - Les cookies Unicode apparaissent aussi dans les sorties de test
-
Python
http.cookies.SimpleCookiede Python arrête silencieusement de charger les cookies suivants lorsqu’il rencontre un cookie JSON- Dans l’exemple, la sortie ne conserve que
cookie1=foo - Si un sous-domaine peut définir un cookie problématique pour le domaine principal, un seul cookie peut casser le traitement de tous les cookies du site
- La gestion des caractères de contrôle y est également irrégulière
- Certains caractères de contrôle sont chargés avec une valeur vide
- Si l’on ajoute
aaavant et après la valeur, le cookie contenant un caractère de contrôle n’est plus chargé
-
Ruby
CGI::Cookie.parsede Ruby semble très tolérant au moment du parsing- Il accepte les caractères de contrôle, tabulations, guillemets doubles, virgules, barres obliques inverses,
0x7Fet caractères Unicode, puis applique un percent-encoding lors de l’extraction depuis le cookie jar - Cette approche est peut-être proche de l’idéal dans l’univers des cookies, mais du code qui définit ses cookies via
document.cookiene s’attend pas forcément à une valeur reflétée en percent-encoding
-
Rust
- Rust ne fournit pas de gestion des cookies par défaut, donc l’observation se base sur la crate populaire
cookie - Dans sa configuration par défaut, la crate
cookiesemble faire partie des options les plus tolérantes et accepte apparemment les chaînes UTF-8 reçues
- Rust ne fournit pas de gestion des cookies par défaut, donc l’observation se base sur la crate populaire
Impact observé sur de vrais sites Web
- Le problème a été découvert lors de la validation manuelle d’une mise à jour de bibliothèque tierce sur un site de test
- Ce type de changement est difficile à attraper par les tests automatisés
- Si la mise à jour avait été déployée telle quelle, les visiteurs suivants auraient pu recevoir un cookie cassé et rester bloqués par des erreurs difficiles à diagnostiquer jusqu’au rollback et à la suppression du cookie
- Le problème ne se limite pas aux petits sites ou à un framework particulier
- En définissant dans la console du navigateur un cookie Unicode comme ci-dessous sur un domaine, on peut casser plusieurs grands sites
document.cookie="unicodeCookie=🍪; domain=.grayduck.mn; Path=/; SameSite=Lax"
- Cas observés
- Facebook : une page d’erreur s’affiche et les images sont aussi cassées
- Instagram et Threads : simple erreur 500
- Netflix : renvoie une erreur
NSES-500et casse aussi la page d’aide - Okta : toutes les pages de connexion renvoient une erreur 400
- WhatsApp : affiche “whatsapp error”
- Amazon : fonctionne en grande partie, mais certaines fonctions cassent de façon aléatoire
- AWS : la console de connexion renvoie une erreur 400 et devient inutilisable
- Apple Support : impossible de charger la liste des appareils
- Best Buy : la navigation fonctionne, mais pas la recherche
- eBay : la plupart des problèmes sont corrigés, mais certaines parties renvoient encore des erreurs 400
- Home Depot : correctif prévu
- Intuit : seul site à avoir identifié la cause de l’erreur
- Outlook : autre cas d’erreur 400
Une correction difficile entre standard et compatibilité
- Corriger un problème dans une spécification de base vieille de 30 ans est extrêmement difficile, et il est possible qu’il n’existe pas de bonne solution
- Le blocage de ces cookies côté navigateur a été étudié et travaillé à la fois par Mozilla et Google
- Mozilla : bug 1797235, CVE-2023-5723, bug 1797231
- Google : bug 40061459
- Un blocage unilatéral est compliqué pour des raisons de compatibilité
- Les cookies non ASCII représentent moins de 0,01 % de l’ensemble des cookies
- Des données de télémétrie montrent qu’ils sont bien plus fréquents dans des pays comme l’Argentine, le Mexique ou la Finlande
- Mozilla a implémenté un paramètre
network.cookie.blockUnicodeactivable rapidement, mais ne l’active pas à cause des problèmes de compatibilité de comportement avec Chromium
- Des correctifs côté serveur sont peut-être possibles, mais ils touchent des millions de sites ainsi que la gestion interne des erreurs dans des langages et frameworks très variés
- Des acteurs comme Facebook ou Netflix pourront peut-être atténuer le problème, mais l’exploitant moyen d’un site n’aura ni le temps ni les moyens de le résoudre
- La solution de fond consiste à ce que l’IETF HTTP Working Group réaligne la spécification des cookies en interne et définisse strictement le fonctionnement des systèmes de traitement des cookies
- L’acceptation ou non des caractères non ASCII devrait être identique côté serveur et côté user agent
- Les étapes de traitement des cookies dans les navigateurs, langages et frameworks devraient être explicites, à l’image de standards W3C modernes comme Content Security Policy
- Un comportement où un seul cookie invalide interrompt aussi le traitement des autres cookies est difficilement acceptable, car il peut provoquer divers incidents imprévus
Procédure de traitement des cookies proposée
- Partir de
field-value, le découper sur;et,pour former une liste deraw-cookie-pair, sans traiter la virgule comme un synonyme du point-virgule - Chaque
raw-cookie-pairest ensuite traité dans cet ordre- S’il n’y a pas de
=, passer au pair suivant - Supprimer les espaces en début et fin
- Considérer ce qui est avant le premier
=commecookie-name-octetset ce qui est après commecookie-value-octets - Si la valeur commence par un guillemet double, en supprimer un au début, puis en supprimer un à la fin s’il est présent
- Si le nom ou la valeur est dans une forme que le serveur ne peut pas accepter, ignorer ce pair
- Les tuples restants
[cookie-name-octets, cookie-value-octets]sont ensuite traités selon la définition du serveur
- S’il n’y a pas de
- Il est en plus proposé que le serveur rejette les tuples dont le nom de cookie n’est pas un token, ainsi que ceux dont la valeur contient des octets qui ne font pas partie de
cookie-octet
1 commentaires
Avis de Hacker News
Les cookies sont pleins de pièges étranges et de comportements pénibles, mais dans 99,95 % des cas, ça fonctionne bien. Mon champ de mines préféré avec les cookies, c’est le shadowing de cookies (cookie shadowing) : si l’on définit des cookies portant le même nom mais avec seulement des attributs clés différents, comme le domaine ou le chemin, on se retrouve avec plusieurs cookies presque identiques en même temps, et côté backend ou JS, il n’y a aucun moyen de savoir lequel est lequel.
Allez sur https://example.com/somepath et saisissez ce qui suit dans la console du navigateur :
document.cookie = "foo=a";document.cookie = "foo=b; domain=.example.com";document.cookie = "foo=c; path=/somepath";document.cookieChez moi, le résultat était
'foo=c; foo=a; foo=b'.C’est une erreur vraiment énorme.
/somepath, recevoir C, la valeur la plus spécifique des trois, paraît assez raisonnable. Comme toutes les valeurs sont renvoyées dans l’ordre, on peut connaître à la fois la valeur propre au chemin et la valeur globale, ce qui donne l’impression d’être le meilleur compromis.En revanche, je n’aime pas le setter magique de
document.cookie, mais l’objet a déjà presque 30 ans, donc on fait avec.Le sujet est récemment revenu avec le durcissement de la validation dans jshttp/cookie : https://github.com/jshttp/cookie/pull/167
Après cette PR, la validation a de nouveau été légèrement assouplie, d’une manière proche du code navigateur mentionné dans l’article.
Le changement initial est parti de notre code, quand nous avons trouvé un bug où l’on construisait un en-tête Cookie en concaténant des chaînes sans encodage. Parfois, des espaces se glissaient dans les valeurs et cassaient les requêtes ; pour éviter cela, nous voulions recommander aux développeurs d’utiliser
serialize()de jshttp/cookie, mais nous nous sommes rendu compte que la validation de cette fonction n’était pas suffisante pour détecter le bug que nous avions vu.Quand nous avons proposé un correctif, quelqu’un d’autre a remarqué que la validation était si laxiste qu’il était possible d’insérer du JS dans le champ nom du cookie, puis de le faire interpréter ailleurs comme une valeur. Cela donnait un chemin d’injection de code assez atypique.
L’article mentionne l’approche de Rust, mais contrairement à d’autres langages, la bibliothèque standard de Rust n’intègre pas de gestion des cookies. En réalité, il s’agit du comportement du crate tiers
cookie, qui propose aussi une option d’encodage en pourcentage, comme Ruby : https://docs.rs/cookie/0.18.1/cookie/On dirait qu’il y a en pratique environ dix mille protocoles différents imbriqués dans le protocole HTTP. Les navigateurs et les serveurs web y ont ajouté toutes sortes de fonctionnalités, chacune avec sa spécification et sa spécification de fait, et tout cela transite presque entièrement sous le même grand parapluie générique appelé HTTP.
Le client ne peut pas indiquer avec quelle version de ces dix mille non-spécifications il est compatible, et le serveur non plus. La raison pour laquelle on ne peut pas faire évoluer les spécifications, c’est que les autres clients ne les comprendraient pas et qu’il n’y a pas non plus de rétrocompatibilité.
Il reste donc un chaos aléatoire sur lequel personne n’arrive à se mettre d’accord et que personne ne peut corriger. Et sans dépréciation planifiée, il faut continuer à traîner les mauvaises décisions du passé.
Il y a environ 10 ans, sur un projet, j’ai implémenté des sessions basées sur les cookies, et j’ai vraiment galéré à déboguer pourquoi l’authentification fonctionnait dans Safari mais pas dans Chrome. Je ne me souviens plus exactement dans quel sens c’était, mais l’un des navigateurs ne définissait tout simplement pas le cookie si le format n’était pas correct.
Nous ne faisions rien de particulièrement étrange ; de mémoire, c’était une différence entre
-et_.Set-Cookie.À l’époque, à cause de ce problème, je n’avais pas pu utiliser
camelCasedans les clés de cookies.Même en cherchant, je n’arrive pas à retrouver le ticket exact.
Dès juste après l’introduction des cookies, il me semble que l’usage raisonnable consistait à n’y mettre que des jetons opaques, afin que le serveur reconnaisse le même client la fois suivante, et à stocker tout le reste côté serveur.
Je ne vois pas pourquoi le fait que le client puisse, en principe, traiter des valeurs que le serveur n’enverrait jamais devrait poser problème. Il suffit de ne pas envoyer ces valeurs, et il n’y a pas besoin de s’inquiéter d’énigmes du genre « que se passerait-il si on l’envoyait ? ».
Cela dit, comme c’est le seul endroit où stocker un jeton opaque, il faut bien les utiliser pour l’authentification.
L’analyse des en-têtes de cookies est un bazar. La « norme » ne reflète pas les comportements qui existent réellement sur le terrain, les serveurs backend, bibliothèques et frameworks n’acceptent pas tous les mêmes formats, et les navigateurs font encore autre chose.
Si l’on contrôle entièrement le frontend et le backend, ce n’est pas un énorme problème, mais dès qu’il faut intégrer des éléments différents, la situation devient très vite absurde.
Les cookies ressemblent à un vaste fouillis complexe, et en même temps ils sont quasiment impossibles à modifier à cause de la rétrocompatibilité. Dans ce genre de cas, il faudrait peut-être créer un tout nouveau mécanisme séparé.
Par exemple, on pourrait spécifier un mécanisme comme NewCookie et le reconcevoir dès le départ pour qu’il se comporte de façon cohérente. On pourrait y intégrer des mesures de sécurité modernes, une spécification plus stricte et une vraie prise en charge d’Unicode.
Set-Cookie2abandonné : https://stackoverflow.com/q/9462180/3474615Du moins pour certains cas d’usage, et bien sûr il n’est pas directement intégré aux en-têtes.
Comme les cookies existent déjà, nous sommes coincés avec eux.
J’ai passé un mois entier à traquer un problème où iOS Safari avalait arbitrairement les cookies de domaines contrôlés par nos clients. Je n’ai jamais vu l’état de session disparaître ainsi sur des domaines comme Google, Twitter ou Facebook.
Plus sérieusement, il vaudrait mieux éviter le mot cookie et lui donner un tout autre nom. Ce mot traîne beaucoup trop de baggage.
L’auteur commence par mettre le résultat de
JSON.stringifydans un cookie, et j’ai trouvé presque surprenant que le problème ne vienne pas du fait que quelqu’un ait mis un point-virgule dans le JSON sérialisé.La plupart des ennuis autour des cookies semblent apparaître quand on essaie d’y mettre une entrée utilisateur arbitraire. Il ne faut pas faire ça. Si l’on n’utilise que des chaînes ASCII alphanumériques de longueur fixe, comme pour les jetons d’authentification, ça va.
Je suis d’accord pour dire que c’est un vrai champ de mines.
En tant que développeur, le contournement consiste à encoder les valeurs en Base64 compatible URL. On obtient alors des octets bruts et on peut utiliser la représentation interne que l’on veut. Cela dit, comme le souligne l’article, on ne contrôle pas tout à 100 %. C’est un user agent, et c’est aussi comme ça que ça doit être.
J’aimerais que davantage de user agents choisissent la conformité aux standards plutôt que les « octets sur le fil et une prière ». Les réponses 400 des captures d’écran sont conformes à la spécification. Il aurait mieux valu que les en-têtes soient en UTF-8 dès le départ, ou qu’ils aient d’abord été en ASCII puis autorisent ensuite l’UTF-8. La première option était toutefois difficile pour des raisons de causalité, et la seconde pouvait encore poser des problèmes, puisqu’elle rendait légales des valeurs auparavant illégales.
base64urln’est pas compatible avecbase64plus un encodage URL dans environ 3 % des cas ; c’est facile à rater en développement, mais ça finit forcément par casser en production.=,/et+, donc on peut aussi utiliser l’encodage Base64 standard :)L’article se moque de la loi de Postel, mais si la partie qui définit le cookie avait été conservatrice dans ce qu’elle envoyait, cet article n’aurait pas été nécessaire dès le départ.
Parfois, ces mines ne sont pas de simples bugs, mais de grosses failles de sécurité.
Si un client envoie des données non conformes à la spécification, c’est un bug et il faut le corriger. Il ne devrait jamais devenir normal qu’un serveur devine l’intention et accepte ces données.