Работа с куки-файлами хуже сапёрного дела

от автора

Если в этом посте вам в основном интересно, как что ломается, сразу можете переходить к последнему разделу.

HTTP-куки — это небольшие информационные добавки, направляемые на клиент с сервера, работающего с JavaScript или HTTP. Куки играют определяющую роль для поддержки состояния во всем вам известной Всемирной Паутине — системе, где иного способа сохранять состояние не предусмотрено. Как только куки установлены, браузеры станут переадресовывать их в нагрузку ко всем HTTP-запросам, у которых правильно выставлена область видимости — до тех пор, пока срок действия куки не истечёт.

Меня более чем устраивало просто не вдаваться в глубокие детали и просто абстрагироваться от того, как именно функционируют куки, пока однажды я не наткнулась на следующий код JavaScript — казалось бы, совершенно безобидный:

const favoriteCookies = JSON.stringify({   ginger: "snap",   peanutButter: "chocolate chip",   snicker: "doodle", });  document.cookie = ` cookieNames=${favoriteCookies}` ;

На уровне браузеров этот код работал совершенно нормально. Он зачерпывал скучного (но вкусного) JSON и сохранял значение в сеансовом куки. Притом, что это выглядело немного странно — ведь обычно код сериализует JSON до base64, а только затем отправляет его в виде куки. Здесь не просматривалось никаких проблем, которые могли бы озадачить браузер. Все браузеры с готовностью разрешали установить куки, после чего посылали их прямо в HTTP-заголовке на веб-сервер базы данных:

GET / HTTP/1.1 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Encoding: gzip, deflate Accept-Language: en-US,en;q=0.9 Connection: keep-alive Cookie: cookieNames={"ginger":"snap","peanutButter":"chocolate chip","snicker":"doodle"} Host: example.com

Всё шло хорошо, пока не пришлось передавать некоторый код, в котором использовалась стандартная библиотека Go. Стандартная библиотека Go оказалась не в состоянии разобрать куки, что привело к лавинообразным отказам вплоть до самой вершины стека. Так что же случилось?

Спецификация

Куки-файлы были впервые описаны в 1997 году в документе RFC 2109, а впоследствии актуализированы в документах RFC 2965 (2000) и RFC 6265 (2011). Также есть ещё один черновой документ, который прямо сейчас в процессе обновления (и именно он используется в этой статье).

В RFC есть два раздела, касающихся значений куки:

Раздел 4.1.1 (о том, как серверы должны отправлять куки)

Неофициально куки содержится в поле Set-Cookie ответного заголовка, который начинается с пары имя-значение, а за этой парой следуют ноль или более пар  атрибут-значение. Серверам НЕ СЛЕДУЕТ отправлять такие заголовочные поля Set-Cookie, которые не соответствуют следующей грамматике:  set-cookie        = set-cookie-string set-cookie-string = BWS cookie-pair *( BWS ";" OWS cookie-av ) cookie-pair       = cookie-name BWS "=" BWS cookie-value cookie-name       = 1*cookie-octet cookie-value      = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE ) cookie-octet      = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E                       ; Символы US-ASCII за исключением управляющих,                       ; пробельного DQUOTE, запятой, точки с запятой                       ; и обратного слэша

Раздел 5.6 (о том, как браузеры должны принимать куки)

Для синтаксического разбора строки set-cookie пользовательский агент ОБЯЗАН использовать алгоритм, эквивалентный следующему:  1. Если в строке set-cookie-string содержится символ %x00-08 / %x0A-1F / %x7F,    (управляющие символы за исключением символа горизонтальной табуляции):      То эти шаги следует отбросить и полностью игнорировать строку set-cookie-string.  2. Если в строке set-cookie-string содержится символ %x3B (";"):    то строка с парой имя-значение состоит из всех символов вплоть до  первого %x3B (";"), но не включая его, а строка unparsed-attributes представляет собой оставшуюся часть    рассматриваемой строки set-cookie-string (включая символ %x3B (";").  Иначе:  1. Строка с парой «имя-значение» состоит из символов, содержащихся в строке  set-cookie-string, а unparsed-attributes представляет собой пустую строку.

Здесь есть три аспекта, которые должны сразу же броситься вам в глаза:

  1. То, что СЛЕДУЕТ отправлять серверам, не соответствует тому, что ОБЯЗАНЫ принимать браузеры. Классический пример трагедии, которая возникает, если придерживаться закона Постела.

  2. Здесь не указано никаких условий, которые диктовали бы, какие куки неприемлемо отправлять их браузера на сервер — не считая оговорки о точке с запятой в качестве разделительного знака. Это было бы нормально, если бы серверы принимали только те куки, которые отправили сами. Но ведь куки могут поступать и из document.cookie и содержать иные значения кроме %x21, %x23-2B, %x2D-3A, %x3C-5B и %x5D-7E, разрешённых в Set-Cookie.

  3. В стандарте не указано, как должны действовать стандартные библиотеки, обрабатывающие заголовки Cookie: как пользовательские агенты или как серверы? Должны ли они действовать по принципу послабления или предписания? Должны ли их действия варьироваться в зависимости от контекста?

В этом и заключается вся суть той проблемы, с которой мне в итоге довелось столкнуться: везде всё устроено по-разному, и просто чудо, что куки вообще работают.

Браузеры

Для начала давайте разберёмся, как работают браузеры. Команды, поддерживающие Gecko (Firefox), Chromium и WebKit (Safari) постоянно взаимодействуют друг с другом, поэтому было бы логично ожидать, что все эти инструменты будут работать одинаково… верно?

Прежде, чем углубиться в детали, напомню: RFC сам себе противоречит, поскольку там сказано, что в заголовках Set-Cookie могут содержаться любые символы кодировки ASCII кроме управляющих, двойных кавычек, запятых, точек с запятой и обратных слэшей, но при этом утверждается, что браузерам следует принимать куки с любыми значениями, если только они не содержат управляющих символов.

Firefox

Код для валидных значений куки в Firefox выглядит примерно так:

bool CookieCommons::CheckValue(const CookieStruct& aCookieData) {   // отклонить куки, если в значении содержатся любые символы, не разрешённые в RFC 6265  - см.   // https://bugzilla.mozilla.org/show_bug.cgi?id=1191423   // ЗАМЕЧАНИЕ: это не полный набор символов, не разрешённых в 6265 – обратите внимание, что    // в этом списке отсутствуют 0x09, 0x20, 0x22, 0x2C и 0x5C.   const char illegalCharacters[] = {       0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x0A, 0x0B, 0x0C,       0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,       0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x3B, 0x7F, 0x00};    const auto* start = aCookieData.value().BeginReading();   const auto* end = aCookieData.value().EndReading();    auto charFilter = [&](unsigned char c) {     if (StaticPrefs::network_cookie_blockUnicode() && c >= 0x80) {       return true;     }     return std::find(std::begin(illegalCharacters), std::end(illegalCharacters),                      c) != std::end(illegalCharacters);   };    return std::find_if(start, end, charFilter) == end; }

† раньше символ 0x7F принимался, и этот баг был исправлен под номером 1797235 (Firefox 108)

Firefox принимает пять таких символов, которые сервер в соответствии с рекомендацией RFC сервер не должен отправлять:

  • 0x09 (горизонтальная табуляция)

  • 0x20 (пробелы)

  • 0x22 (двойные кавычки)

  • 0x2C (запятые)

  • 0x5C (обратные слэши)

Исходно это было сделано давным-давно для обеспечения паритета с Chrome, и следы этого решения до сих пор сохранились в обеих базах кода.

Самые внимательные из вас могли заметить, что в Firefox есть настройка network.cookie.blockUnicode, с которой сверяется этот код, и которая отвергает любые значения выше 0x80. Данная базовая работа была проделана в результате долгого исследования, и за её ходом можно проследить в отчёте по багу 1797231.

Chromium

В Chromium код для валидных куки выглядит примерно так:

bool ParsedCookie::IsValidCookieValue(const std::string& value) {   // Функция IsValidCookieValue() возвращает, соответствует ли строка следующей   // грамматике:   //   // cookie-value       = *cookie-value-octet   // cookie-value-octet = %x20-3A / %x3C-7E / %x80-FF   //                       ; octets excluding CTLs and ";"   //   // Таким образом можно определять, содержатся ли в значениях куки    // какие-либо недопустимые символы.   //   // Обратите внимание: в разделе 4.1.1 документа RFC6265bis section 4.1.1 предлагается более сложная    // грамматика для разбора значений куки, но мы предпочитаем разрешить более широкий диапазон символов,    // чем допускается в рамках этой грамматики (не противореча при этом требованиям, предъявляемым к   // алгоритму разбора, которые определены в разделе 5.2).   //   // Для справки см.:   //  - https://crbug.com/238041   for (char i : value) {     if (HttpUtil::IsControlChar(i) || i == ';')       return false;   }   return true; }  // В приложении B.1 указано,  // является ли конкретный символ управляющим (CTL) в соответствии с документом RFC 5234 static inline bool IsControlChar(char c) {   return (c >= 0x00 && c <= 0x1F) || c == 0x7F; }

В Chrome ограничения чуть строже, чем в Firefox — он отказывается принимать в значениях куки символ 0x09 (горизонтальная табуляция).

Тем не менее (что, кстати, противоречит RFC), он с готовностью принимает и отправляет пробелы, двойные кавычки, запятые, обратные слэши и символы Юникода.

Safari (WebKit)

Мне не удалось получить доступ к коду хранилища куки, поскольку этот код находится в закрытом исходнике CFNetwork. Тем не менее, мне удалось заглянуть внутрь браузерного движка, выполнив следующий код на JavaScript:

for (i=0; i<256; i++) {   let paddedIndex = i.toString().padStart(3, '0') +     '_' + '0x' + i.toString(16).padStart(2, '0');    // установить куки с именем "cookie" + десятичный символ + шестнадцатеричный символ   // и значением символа, заключённым в пробелы и двойные дефисы    document.cookie=` cookie${paddedIndex}=-- ${String.fromCharCode(i)} --` ; }  document.cookie='cookieUnicode=🍪';  :::text cookie007_0x07   --           localhost   /   Session   16 B cookie008_0x08   --           localhost   /   Session   16 B cookie009_0x09   --      --   localhost   /   Session   21 B cookie010_0x0a   --           localhost   /   Session   16 B cookie011_0x0b   --           localhost   /   Session   16 B                    (snip for brevity) cookie030_0x1e   --           localhost   /   Session   16 B cookie031_0x1f   --           localhost   /   Session   16 B cookie032_0x20   --   --      localhost   /   Session   21 B cookie033_0x21   -- ! --      localhost   /   Session   21 B cookie034_0x22   -- " --      localhost   /   Session   21 B cookie035_0x23   -- # --      localhost   /   Session   21 B                    (snip for brevity) cookie042_0x2a   -- * --      localhost   /   Session   21 B cookie043_0x2b   -- + --      localhost   /   Session   21 B cookie044_0x2c   --,--        localhost   /   Session   19 B                    (snip for brevity) cookie044_0x5c   -- \ --          localhost   /   Session   19 B

Поскольку Safari прекращает обработку конкретного куки, как только встречает недопустимый символ, легко понять, что допустимы 0x09 (горизонтальная табуляция), 0x20 (пробел), 0x22 (двойные кавычки) и 0x5C (обратный слэш), но недопустимы 0x7F (удаление), 0x80-FF (символы из верхней части таблицы ASCII / Юникод).

В отличие от Firefox и Chrome, где соблюдается приведённая в RFC инструкция «отбросить эти шаги и полностью проигнорировать куки» в случае, когда в попавшемся куки содержатся управляющие символы, браузер Safari не игнорирует такой куки, а принимает часть этого значения вплоть до управляющего символа.

Как ни странно, эти исследования навели меня на причудливый баг Safari: если задать куки со значением — , — , то, по-видимому, пробелы по обе стороны от запятой убираются.

Стандартные библиотеки

Golang

Начнём с кода для работы с куки в Golang: именно в нём я впервые столкнулась с описываемой проблемой.

// sanitizeCookieValue производит из v допустимое значение куки. // https://tools.ietf.org/html/rfc6265#section-4.1.1 // // Ослабляем это требование, поскольку пробелы и запятые в значениях куки очень распространены, но  // заключаем готовое значение куки в кавычки тогда и только тогда, когда в v содержатся // запятые или пробелы. // Подробное обсуждение см. в https://golang.org/issue/7243  func sanitizeCookieValue(v string) string {     v = sanitizeOrWarn("Cookie.Value", validCookieValueByte, v)     if len(v) == 0 {         return v     }     if strings.ContainsAny(v, " ,") {         return `"` + v + `"`     }     return v }  func validCookieValueByte(b byte) bool {     return 0x20 <= b && b < 0x7f && b != '"' && b != ';' && b != '\\' }

В Golang почти буквально соблюдается формулировка из RFC о том, как серверы должны обращаться с Set-Cookie, лишь с той разницей, что допускаются символы 0x20 (пробел) и 0x2C (запятая), поскольку на практике они повсеместно встречаются в куки.

Вы уже представляете, с какими препонами приходится иметь дело в разных языках программирования. Код должен одновременно и получать значения из браузеров в соответствии с разделом 5, но при этом и отправлять куки именно так, как это описано в разделе 4.1.1.

Последствия у этого могут быть самые серьёзные, и в этом можно убедиться, выполнив следующий код:

package main  import (   "fmt"   "net/http" )  func main() {   rawCookies :=     ` cookie1=foo; ` +     ` cookie2={"ginger":"snap","peanutButter":"chocolate chip","snicker":"doodle"}; ` +     ` cookie3=bar`      header := http.Header{}     header.Add("Cookie", rawCookies)     request := http.Request{Header: header}      fmt.Println(request.Cookies()) }

Дающий такой скромный вывод:

[cookie1=foo cookie3=bar]

Незаметно отбрасываем куки, принимаемый всеми основными браузерами, и не сопровождаем код никаким исключением, из которого было бы понятно, что это произошло. Правда, если можно отбросить куки, непонятные браузеру — и опять без каких-либо побочных эффектов, то на практике всё будет гораздо лучше, чем могло бы.

PHP

Во многих языках, например в PHP, не предусмотрены нативные функции для разбора куки. Поэтому становится непросто с определённостью сказать, что в этом языке разрешено, а что нет.

При этом можно задать куки, воспользовавшись следующим кодом — и посмотреть, как отреагирует PHP:

[0x09, 0x0D, 0x10, 0x20, 0x22, 0x2C, 0x5C, 0x7F, 0xFF].forEach(i => {   let paddedIndex = i.toString().padStart(3, '0') + '_' +     '0x' + i.toString(16).padStart(2, '0');    document.cookie=` cookie${paddedIndex}=-- ${String.fromCharCode(i)} --` ; });  document.cookie='cookieUnicode=🍪';

Вывод

cookie009_0x09: -- -- cookie009_0x10: -- -- cookie009_0x0d: -- -- cookie032_0x20: -- -- cookie034_0x22: -- " -- cookie044_0x2c: -- , -- cookie092_0x5c: -- \ -- cookie255_0x7f: -- -- cookie255_0xff: -- ÿ -- cookieUnicode: 🍪

Когда приходится иметь дело с управляющими символами, PHP действует совершенно нелогично. Все значения из диапазона 0x00-0x09 работают нормально, равно как и 0x0D (возврат каретки), но если вы воспользуетесь 0x10 (освобождение канала) или 0x7F (удаление), то PHP без вариантов выдаст ошибку 400 Некорректный запрос.

Python

import http.cookies  raw_cookies = (     'cookie1=foo; '     'cookie2={"ginger":"snap","peanutButter":"chocolate chip","snicker":"doodle"}; '     'cookie3=bar' )  c = http.cookies.SimpleCookie() c.load(raw_cookies)  print(c)

Вывод:

>>> Set-Cookie: cookie1=foo

Python незаметно отменяет загрузку дополнительных куки внутри SimpleCookie.load(), как только ему попадается любой непонятный куки. Это может быть очень опасно; только представьте себе, что в поддомене вполне может быть задан куки для базового домена — и из-за этого полностью нарушатся все куки для всех доменов конкретного сайта.

Путаница только нарастает, когда приходится иметь дело с управляющими символами:

import http.cookies  for i in range(0, 32):     raw_cookie = f"cookie{hex(i)}={chr(i)}"      c = http.cookies.SimpleCookie()     c.load(raw_cookie)      for name, morsel in c.items():         print(f"{name}: value: {repr(morsel.value)}, length: {len(morsel.value)}")

Вывод:

>>> cookie0x9: value: '', length: 0 >>> cookie0xa: value: '', length: 0 >>> cookie0xb: value: '', length: 0 >>> cookie0xc: value: '', length: 0 >>> cookie0xd: value: '', length: 0

Как видите, здесь Python незаметно отбрасывает множество куки с управляющими символами и неправильно загружает другие. Обратите внимание: если попытаться оградить эти значения по принципу:

raw_cookie = f"cookie{hex(i)}=aa{chr(i)}aa"

то не загрузится ни один куки с управляющими символами. Вообще Python крайне непоследователен и непредсказуем в том, что касается загрузки куки.

Ruby

require "cgi"  raw_cookie = 'ginger=snap; ' + "cookie=chocolate \x13 \t \" , \\ \x7f 🍪 chip; " + 'snicker=doodle'  cookies = CGI::Cookie.parse(raw_cookie)  puts cookies puts cookies["cookie"].value() puts cookies["cookie"].value().to_s()

Вывод:

{"ginger"=>#<CGI::Cookie: "ginger=snap; path=">, "cookie"=>#<CGI::Cookie: "cookie=chocolate+%13+%09+%22+%2C+%5C+%7F+%F0%9F%8D%AA+chip; path=">, "snicker"=>#<CGI::Cookie: "snicker=doodle; path=">} chocolate    " , \  🍪 chip cookie=chocolate+%13+%09+%22+%2C+%5C+%7F+%F0%9F%8D%AA+chip; path=

В библиотеке Ruby для работы с куки, по-видимому, многое разрешено. Язык на этапе разбора принимает любые символы, а затем, подтягивая их из хранилища куки (cookie jar) кодирует их в процентном представлении.

Возможно, это оптимальное поведение (если допустить, что подобный вариант может сосуществовать с куки), но я определённо могу вообразить случаи, в которых при задании куки через document.cookie мы совершенно не ожидаем получить их обратно уже в процентном представлении.

Rust

use cookie::Cookie;  fn main() {     let c = Cookie::parse("cookie=chocolate , \" \t foo \x13 ñ 🍪 chip;").unwrap();     println!("{:?}", c.name_value()); }

Вывод:

("cookie", "chocolate , \" \t foo \u{13} ñ 🍪 chip")

В Rust по умолчанию не предоставляется никаких возможностей обработки куки, поэтому за ними приходится обращаться в популярный пакет cookie. По-видимому, в конфигурации по умолчанию Rust — самый либеральный из всех языков программирования в этом отношении. Ему просто бросают любую строку в кодировке UTF-8, и он её принимает.

Почему всё это действительно важно при работе во Всемирной Паутине

Учитывая все эти безумные различия в поведении от браузера к браузеру и от языка к языку, тут определённо набирается материал на несколько увлекательных таблиц, но как всё это проявляется в реальной практике?

Когда мне впервые довелось обнаружить всё это на реальных примерах, я поняла, насколько мне повезло, что не случилось никакой катастрофы. Тестировщик вручную проверял обновление сторонней библиотеки и при работе наткнулся на странный набор ошибок. Если бы он специально мне на это не указал, то данное обновление — а тонкости его работы едва ли можно было отловить при автоматизированном тестировании — поступило бы в продакшен. В результате все последующие посетители сайта получали бы повреждённые куки, и их выбрасывало бы с сайта в сопровождении невразумительной ошибки. Так происходило бы до тех пор, пока мы не откатили бы обновление и не почистили все куки.

Причём такая проблема возникает именно из-за того, что спецификацию можно трактовать двояко. Подобную ошибку настолько легко допустить на миллионах сайтов, что можно считать — от тотального краха их может спасать лишь какой-нибудь стажёр. Всё это актуально не только для крошечных сайтов и экзотических фреймворков, но и для таких гигантов как Netflix, WhatsApp и Apple.

Можете сами убедиться, насколько легко допустить такую ошибку. Если тестируете какой-нибудь сайт, то просто вставьте в консоль следующий простой фрагмент кода — при этом замените .grayduck.mn на тестируемый вами домен:

document.cookie="unicodeCookie=🍪; domain=.grayduck.mn; Path=/; SameSite=Lax"

Netflix

Netflix возвращает ошибку NSES-500

Netflix возвращает ошибку NSES-500
К сожалению, справочная страница тоже поломана

К сожалению, справочная страница тоже поломана

Okta

Любая страница для входа на сайт Okta возвращает ошибку 400

Любая страница для входа на сайт Okta возвращает ошибку 400

WhatsApp

Сами видите, что происходит с Whatsapp

Сами видите, что происходит с Whatsapp

Amazon

Amazon большей частью работает, но местами отказывает

Amazon большей частью работает, но местами отказывает

Amazon Web Services

Консоль для входа в AWS возвращает ошибку 400 и аварийно завершается

Консоль для входа в AWS возвращает ошибку 400 и аварийно завершается

Apple Support

На странице поддержки от Apple не удаётся загрузить данные ваших устройств

На странице поддержки от Apple не удаётся загрузить данные ваших устройств

Best Buy

Навигация по сайту работает, а поисковик — нет

Навигация по сайту работает, а поисковик — нет

eBay

В основном исправлено, но некоторые элементы eBay по-прежнему выдают ошибку 400

В основном исправлено, но некоторые элементы eBay по-прежнему выдают ошибку 400

Home Depot

Home Depot обещает всё поправить

Home Depot обещает всё поправить

Intuit

Только на Intuit причина ошибки была определена верно

Только на Intuit причина ошибки была определена верно

Outlook

Ещё одна ошибка 400. Теперь вы убедились, насколько системная эта проблема

Ещё одна ошибка 400. Теперь вы убедились, насколько системная эта проблема

Как же мы это исправили?

Стоит ли удивляться, что очень, очень сложно исправлять проблемы, заложенные в базовой спецификации, которой три десятка лет. Причём едва ли есть хорошее решение для такой проблемы.

Ранее был испробован вариант с блокированием куки на стороне браузера — и он сработал в Mozilla и Google:

Но оказывается, что односторонняя блокировка достаточно сложна. Притом, что куки, содержащие иные символы, кроме ASCII, не так уж распространены (на них приходится чуть менее 0,01% всех куки), они по данным телеметрии значительно более распространены в таких странах как Аргентина, Мексика и Финляндия. В браузере Mozilla действительно внедрили настройку, на которую можно быстро переключиться (network.cookie.blockUnicode), но она не активирована, поскольку вызывает проблемы с совместимостью между Mozilla и Chromium.

Потенциально возможно исправить эту проблему на стороне сервера, но такие изменения затронут миллионы сайтов, а большинство ошибок, возникающих из-за этой проблемы, скрыты глубоко внутри языков программирования и веб-фреймворков. Подобные проблемы можно сгладить в отдельно взятом Netflix, но у оператора среднего сайта не будет ни времени, ни компетенции, чтобы с ней справиться.

Истина такова, что как следует исправить эту проблему можно только силами рабочей группы IETF HTTP — она должна обновить спецификацию куки, чтобы документ приобрёл самосогласованность, а также строже регламентировал, как именно следует обращаться с куки в системах. (Не)допустимость символов, не относящихся к ASCII должна идентично трактоваться как на стороне сервера, так и в пользовательских агентах.

Причём, как бы то ни было, обработка куки в браузерах, языках программирования и фреймворках должна строиться явно, во многом по образцу современных стандартов W3C, таких как Политика безопасности контента. Если отвергать обработку тех или иных куки из-за того, что какие-то куки неаккуратно оформлены или неприемлемы, то можно столкнуться с разнообразными неожиданными поведениями.

Пожалуй, правильная последовательность обработки должна строиться как-то так:

• Начинаем со значения поля • Делаем разбиение по ; и ,, чтобы получить список "raw-cookie-pair". Запятая НЕ считается синонимом точки с запятой, так мы обеспечиваем комбинации заголовков, несмотря на то, что указано в разделе 3.2.2 спецификации RFC 7230.  Для каждой пары raw-cookie-pair:   ◦ Если в паре не содержится =, то переходим к следующей raw-cookie-pair   ◦ Убираем ведущие и хвостовые пробелы    ◦ Всю информацию до первого = трактуем как cookie-name-octets   ◦ Всю информацию после первого = трактуем как cookie-value-octets   ◦ Если cookie-value-octets начинается с DQUOTE, то:     ‣ Удаляем одну DQUOTE в начале     ‣ Удаляем одну DQUOTE в конце, если она там есть   ◦ Если полученные в результате cookie-name-octets, cookie-value-octets или оба фрагмента информации неприемлемы для сервера, то переходим к следующей raw-cookie-pair   ◦ Процессы объединяются в кортеж [cookie-name-octets, cookie-value-octets] в соответствии с правилами, определёнными на сервере   В дальнейшем серверам СЛЕДУЕТ обрабатывать cookie-name-octets и отклонять любые кортежи, где cookie-name-octets — не токен.  В дальнейшем серверам СЛЕДУЕТ обрабатывать любые cookie-value-octets и отклонять любой кортеж, в котором cookie-value-octets содержат октеты, отсутствующие в cookie-octet.

Общая таблица

Упр. символы1

htab

space

dquote

comma

backslash

delete

0x80-FF
(и Unicode)

RFC 6235 (Раздел 4.1.1)

Нет

Нет

Нет

Нет

Нет

Нет

Нет

Нет

RFC 6265 (Раздел 5.6)

Нет

Да

Да

Да

Да

Да

Нет

??

Firefox

Нет2

Да

Да

Да

Да

Да

Да

Да

Chromium

Нет

Нет

Да

Да

Да

Да

Нет

Да

Safari

Нет3

Да

Да

Да

Да4

Да

Нет

Нет

Golang

??

Нет

Да

Нет

Да

Нет

Нет

Нет

Python

Да5

Нет

Нет

Нет

Да

Нет

Нет

Нет

Ruby (CGI)

Да

Да

Да

Да

Да

Да

Да

Да

Rust

Да

Да

Да

Да

Да

Да

Да

Да

  1. Не так, как определено в RFC 5234, а как в \x00-08 и \x0A-x1F (напр., управляющие символы за исключением горизонтальной табуляции и удаления)

  2. Mozilla больше не разрешает управляющие символы в document.cookie по состоянию на Firefox 108

  3. Не прекращать обработку и игнорировать куки в соответствии с RFC

  4. По‑видимому, в некоторых случаях удаляются пробелы, обрамляющие запятую

  5. Иногда допускаются 0x0A-0D, но их не удаётся сохранять в значении куки, в других случаях обработка прекращается.

Благодарности

Эта статья никогда бы не состоялась без всесторонней помощи со стороны коллег. Хотела бы поблагодарить:

  • По-Нинь Цзэна за то, что помог мне разобраться в этой проблеме

  • Дэна Ведитца из Mozilla за его исчерпывающие знания и бесконечную любезность

  • Фредерика Брауна за полезную обратную связь на раннем этапе работы

  • Стивена Бинглера из Google за настойчивость, благодаря которой описанная проблема была устранена

  • Питера Боуэна за его мысли о том, как следовало бы правильно организовать обработку куки

  • Криса Палмера и Дэвида Шинази за вдумчивую вычитку текста

  • Штефана Бюлера, который уже более десяти лет назад обнаружил некоторые описанные здесь явления.


ссылка на оригинал статьи https://habr.com/ru/articles/861300/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *