📱 Подписаться
IT и цифровая трансформация

Cookies, которых не видно: partitioned cookies, cookies.txt и одно расширение под три браузера

📰 Habr 👁️ 0 просмотров

Perruer59 минут назад

Cookies, которых не видно: partitioned cookies, cookies.txt и одно расширение под три браузера

Уровень сложностиСреднийВремя на прочтение5 минОхват и читатели1.4KБраузерыИнформационная безопасность*JavaScript*Open source*FirefoxКейсБывает так: открываете менеджер cookies, удаляете всё для сайта, перезагружаете страницу — а сайт вас по-прежнему узнаёт. Одна из причин в том, что современные браузеры хранят часть cookies в отдельных «разделах», и старые расширения их просто не видят.

Я разбирался с этим, когда переписывал Cookie Quick Manager — популярный менеджер cookies для Firefox (около 45 тысяч пользователей на AMO), который не обновлялся с 2019 года. Результат — CookieTin, открытое расширение для Firefox, Chrome и Edge (GitHub). В статье — что такое partitioned cookies, как их достать через WebExtensions API и на какие грабли я наступил по дороге.

CookieTin

Что такое partitioned cookies

Раньше cookie стороннего сайта была одна на весь браузер. Если виджет widget.example был встроен в сотню сайтов, он видел одну и ту же cookie на каждом из них. На этом и держалась слежка между сайтами.

Сейчас браузеры это ломают:

• Firefox, Total Cookie Protection (включена по умолчанию): cookies встроенного сайта хранятся отдельно для каждого сайта верхнего уровня. widget.example на news.example и тот же widget.example на shop.example получают две разные «банки» cookies.
• Chrome, CHIPS (Cookies Having Independent Partitioned State): сайт сам помечает cookie атрибутом Partitioned, и она хранится отдельно для каждого сайта верхнего уровня.В WebExtensions API такие cookies получили поле partitionKey:

{
"name": "session",
"domain": "widget.example",
"partitionKey": { "topLevelSite": "https://news.example" }
}

Почему менеджеры их не видят

Вот главный подвох: cookies.getAll()по умолчанию partitioned cookies не возвращает. И в Firefox, и в Chrome. Никакой ошибки нет, их просто нет в выдаче.

Чтобы получить все cookies, в фильтр нужно передать пустой partitionKey. А в Firefox ещё и firstPartyDomain: null, иначе вы не увидите cookies режима First-Party Isolation:

export async function getAll(filter: Record<string, unknown> = {}): Promise<Cookie[]> {
const base: Record<string, unknown> = { ...filter };
if (isFirefox) base.firstPartyDomain = null;
if (partitionQuerySupported !== false) {
try {
const list = await ext.cookies.getAll({ ...base, partitionKey: {} });
partitionQuerySupported = true;
return list;
} catch (e) {
// Старый Chromium не знает partitionKey в фильтре
if (partitionQuerySupported === true) throw e;
partitionQuerySupported = false;
}
}
return await ext.cookies.getAll(base);
}С удалением то же самое. cookies.remove() без точного partitionKey просто не найдёт cookie. Именно поэтому у пользователей Cookie Quick Manager некоторые cookies «не удалялись никак» (issue #121): расширение удаляло cookie с тем же именем и доменом, но из другого раздела.

Из этого следует ещё одна вещь, которую легко упустить: ключ уникальности cookie — не(domain, path, name). Две cookies с одинаковыми именем, доменом и путём могут одновременно жить в разных разделах и контейнерах. В CookieTin ключ такой:

[storeId, domain, path, name, firstPartyDomain, partitionKey.topLevelSite, partitionKey.hasCrossSiteAncestor]В интерфейсе у таких cookies показывается, для какого сайта они хранятся. Когда вы удаляете cookies сайта из меню на панели, удаляются и partitioned cookies встроенных в него сайтов.

Как пересоздать cookie так, чтобы браузер её принял

Редактирование cookie в WebExtensions — это cookies.set() с новыми значениями. Кажется, что можно взять объект из getAll() и передать обратно. Но нет, по дороге есть несколько неочевидных правил.

Host-only cookie ставится безdomain. Если передать domain: "example.com", браузер создаст cookie .example.com, которая видна поддоменам. Это уже другая cookie. Чтобы получить host-only, поле domain нужно вообще не передавать:

if (!c.hostOnly) d.domain = c.domain.startsWith(".") ? c.domain : "." + c.domain;В Firefox нетsameSite: "unspecified". Chrome его принимает, Firefox выбрасывает ошибку. Решение — просто не передавать поле, тогда подставится значение по умолчанию.

Браузер отказывает без объяснений. Cookie __Host- с полем domain, SameSite=None без Secure или дата истечения в прошлом — и cookies.set() вернёт null или бросит невнятную ошибку. Поэтому в редакторе CookieTin эти правила проверяются до сохранения и объясняются человеческим языком.

cookies.txt: формат, который все пишут чуть по-разному

cookies.txt (формат Netscape) нужен для curl, wget, yt-dlp и gallery-dl. Формат простой — семь колонок через табуляцию:

.example.com TRUE / TRUE 1893456000 session abc123
#HttpOnly_example.com FALSE / TRUE 0 token xyzВторая колонка означает «включать поддомены» (TRUE/FALSE). HttpOnly-cookies пишутся с префиксом #HttpOnly_. Старые парсеры принимают такие строки за комментарии и теряют их.

Cookie Quick Manager писал эту колонку наоборот и строчными буквами: true у него означало «только этот хост». Из-за этого yt-dlp получал неправильные cookies (issue #96). При импорте CookieTin различает оба варианта по регистру. Реальные файлы из curl и браузеров пишут TRUE/FALSE, а старые файлы из CQM — true/false:

const cqm = flag === "true" || flag === "false";
const hostOnly = domain.startsWith(".") ? false : cqm ? flag === "true" : flag !== "TRUE";

Защищённые cookies

Одна из любимых функций у пользователей — «защитить cookie», чтобы очистка её не трогала. В CookieTin защищённая cookie ещё и возвращается на место, если её удалил сайт или вы сами в настройках браузера:

ext.cookies.onChanged.addListener(async (info) => {
if (!info.removed || info.cause !== "explicit") return; // истекшие и перезаписанные не трогаем
if (!settings.guardProtected || !isProtected(map, info.cookie)) return;
await setCookie(info.cookie);
});Фильтр cause === "explicit" важен. Без него расширение воскрешало бы и cookies с истёкшим сроком, и cookies, которые сайт просто перезаписал новым значением.

Один код на Firefox, Chrome и Edge

Расширение собирается под Manifest V3 из одной кодовой базы: TypeScript, Preact и esbuild. Различия браузеров спрятаны в нескольких местах:

• Хранилища cookies. В Chrome это "0" (обычное) и "1" (инкогнито). В Firefox — firefox-default, firefox-private и отдельные хранилища контейнеров Multi-Account Containers (contextualIdentities).
• Манифест. Для Firefox добавляется browser_specific_settings с ID расширения и data_collection_permissions (его теперь требует AMO), для Chrome — ничего лишнего.
• Фоновый код. В MV3 фоновая страница засыпает. Все таймеры (периодическая очистка) переведены на chrome.alarms, а состояние живёт в storage, а не в памяти.Отдельная история — пользователи старого расширения. Firefox позволяет выпустить новую версию под тем же ID дополнения. Для этого случая CookieTin умеет собираться с ID Cookie Quick Manager и при обновлении сам переносит защищённые cookies и настройки из старого формата хранения. Для тех, кто просто установит CookieTin рядом, есть импорт резервной копии CQM.

Тестирование

E2E-тесты на Puppeteer гоняются в Chrome for Testing, Firefox и Edge. Тест создаёт через API обычные, HttpOnly и partitioned cookies (с partitionKey: { topLevelSite: "https://example.com" }), открывает менеджер и проверяет всё подряд: список с пометкой раздела, удаление partitioned cookie (то, что не умел Cookie Quick Manager), редактирование, экспорт и импорт.

Самая неприятная часть — Firefox. Установить временное дополнение через WebDriver BiDi можно (installExtension), но чтобы открыть его страницы, нужен внутренний UUID расширения. Его приходится фиксировать через префы профиля (extensions.webextensions.uuids).

Отдельный тест проверяет обновление: ставит настоящий Cookie Quick Manager 0.5rc2 из исходников, добавляет защищённые cookies, обновляет до CookieTin и проверяет, что всё переехало.

Итог

• Видит и удаляет partitioned cookies (TCP/CHIPS), cookies контейнеров и приватных окон.
• Редактор проверяет правила браузера до сохранения, декодирует URL, Base64 и JWT.
• Импорт и экспорт в JSON (Cookie-Editor, EditThisCookie), cookies.txt с правильной семантикой, заголовок Cookie, формат Cookie Quick Manager, Playwright.
• Защищённые cookies, автоочистка по таймеру и при запуске.
• Firefox (включая Android), Chrome, Edge. Без сети, аналитики и аккаунтов. Лицензия GPL-3.0, как у оригинала.Код и релизы: https://github.com/Perruer/cookietin

Если расширение пригодится, поддержать разработку можно наBoosty.Теги:• cookies
• partitioned cookies
• chips
• total cookie protection
• webextensions
• manifest v3
• cookies.txt
• firefox
• chrome
• расширения браузераХабы:• Браузеры
• Информационная безопасность
• JavaScript
• Open source
• FirefoxЕсли эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопкуЗадонатить

Получайте больше инсайтов о систематизации бизнеса

Подписывайтесь на Telegram-канал Business Operations — ежедневные материалы о бизнес-процессах, операционном управлении и повышении эффективности

💬 Подписаться на канал