Canvas, WebGL и Audio Fingerprinting: как браузер выдает себя без cookies

Canvas, WebGL, Audio и video fingerprinting измеряют особенности рендеринга и обработки медиа, превращая их в вероятностный идентификатор браузера.
В отличие от cookie, этот сигнал не записывается в хранилище сайта. Скрипт вызывает обычные веб-API, собирает ответы и сравнивает их с прошлыми визитами. Для антифрода это сигнал риска, для приватности это повод контролировать данные профиля. Отпечаток браузера не является паспортом человека: драйвер может обновиться, а комбинация признаков всё равно способна связать сессии.
Один Canvas-хеш ничего не доказывает. Система сопоставляет метрики текста, пиксели, GPU, AudioContext, часовой пояс, язык и поведение сессии. Несогласованность признаков часто информативнее одного значения, и именно на этой несогласованности построено большинство современных детекторов ботов.
Что измеряют Canvas, WebGL, Audio и video API
Fingerprinting измеряет свойства среды, а не читает сохранённый ID. Ответы нормализуют, объединяют с другими признаками и хешируют. Один скрипт на двух машинах может вернуть разные байты из-за ОС, шрифтов, драйвера или версии движка.
Canvas fingerprinting читает пиксели или геометрию после рисования текста и фигур. Canvas fingerprinting чувствителен к шрифтам и стеку растеризации текста. WebGL добавляет расширения, форматы точности, vendor/renderer и результат 3D-рендеринга. WebGL fingerprint получают из параметров API и из буфера после работы шейдера. Audio fingerprinting обычно использует OfflineAudioContext, а video fingerprinting опирается на кодеки, MediaCapabilities и WebCodecs. У каждого из этих четырёх векторов своя «физическая» причина отличаться между устройствами, поэтому их стоит рассматривать отдельно, прежде чем сводить в общий скоринг.
| Метод отпечатка | Что именно измеряет | Ориентировочная энтропия (бит) | Устойчивость к рестарту / обновлениям | Как собирает скрипт (API) |
|---|---|---|---|---|
| Canvas 2D | растеризацию текста, kerning, hinting, сглаживание | 5–8 бит | высокая, меняется только после обновления шрифтов, ОС или GPU-драйвера | toDataURL(), getImageData() |
| WebGL / WebGL2 | GPU vendor/renderer, precision шейдера, ANGLE backend | 6–10 бит | высокая, связана с физическим железом и драйвером | getParameter(), readPixels(), WEBGL_debug_renderer_info |
| AudioContext | DSP-обработку осциллятора и компрессора, округление floating-point | 3–5 бит | средняя, зависит от версии аудиодвижка браузера | OfflineAudioContext, буфер Float32Array |
| WebCodecs / MediaCapabilities | поддержку кодеков, аппаратное декодирование, цветовой гамут | 4–7 бит | средняя, меняется при обновлении GPU-драйвера или ОС | decodingInfo(), isConfigSupported() |
Эти диапазоны ориентировочные и построены по логике исследований вроде Panopticlick и AmIUnique: точная цифра всегда зависит от конкретной популяции пользователей, а не является универсальной константой. Далее в статье мы вернёмся к этой таблице, когда объясним, что означает «бит энтропии» на практике.
Как рендеринг текста и GPU создают измеримый след
Рендеринг текста в браузере проходит через несколько независимых подсистем ОС. В Windows за растеризацию глифов отвечает DirectWrite, в Linux типично FreeType, в macOS CoreText. Каждая из них по-своему реализует hinting, kerning и субпиксельное сглаживание, где красный, зелёный и синий субпиксель дисплея получают разную интенсивность. Далее растровое изображение проходит цветовую коррекцию: ICC-профиль монитора и рабочее пространство sRGB способны немного сместить яркость каналов ещё до того, как пиксели попадут в getImageData().
Сам Chromium рисует 2D Canvas через библиотеку Skia, и разные сборки Skia или версии Chromium могут чуть иначе округлять контуры букв или применять anti-aliasing. GPU-драйвер добавляет ещё один слой: аппаратное ускорение композитинга зависит от версии драйвера и модели видеокарты. В итоге нарисовать одну и ту же строку текста на двух формально одинаковых машинах, но с разными GPU-драйверами, это как попросить двух разных людей расписаться одной и той же ручкой: буквы похожи, но наклон, нажим и мелкие завитки на уровне десятитысячных долей пикселя всегда чуть отличаются.
Скрипт может нарисовать разные алфавиты, emoji и градиент, затем вызвать getImageData(). Если p_i это канал i-го пикселя, измерение выглядит как v = (p_1, p_2, …, p_n), а ключ как H(v || metadata). Хеш лишь упрощает сравнение: антифрод часто использует расстояние между векторами или кластеры, а не точное совпадение хешей.
// Безопасный пример для внутренней тестовой страницы
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
ctx.font = '16px sans-serif';
ctx.fillText('Afina Ω 123', 8, 24);
const pixels = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
console.log('sample length', pixels.length);
Пример показывает механику измерения. Production-системам не следует передавать сырые пиксели между доменами или решать вопрос фрода по одному рендеру.
WebGL и WebGL2 под микроскопом: движок, точность и ANGLE
WebGL добавляет к Canvas отдельный пласт данных о графическом конвейере. Расширение WEBGL_debug_renderer_info позволяет прочитать два параметра, UNMASKED_VENDOR_WEBGL и UNMASKED_RENDERER_WEBGL, которые показывают реального производителя и модель GPU вместо обобщённой строки вроде «Google Inc.». Параллельно gl.getParameter() раскрывает десятки характеристик железа: MAX_TEXTURE_SIZE, MAX_VERTEX_UNIFORM_VECTORS, ALIASED_LINE_WIDTH_RANGE и другие лимиты, зависящие от конкретного чипа и версии драйвера.
Ещё одно измерение, точность вычислений в шейдерах. Через gl.getShaderPrecisionFormat() можно узнать, как GPU округляет highp float во фрагментном шейдере: стандарт IEEE 754 задаёт базовые правила double- и single-precision floating point, но конкретная реализация на мобильном GPU и на десктопном GPU округляет последние разряды по-разному. Эти микроскопические расхождения накапливаются в вычислениях и отражаются в результате readPixels() после рендера тестовой сцены.
Технически вызовы WebGL не обращаются к железу напрямую, а проходят через ANGLE, слой, транслирующий команды WebGL в нативный графический API конкретной платформы: Direct3D 11 в Windows, Metal в macOS, Vulkan всё чаще в Android и Linux, либо OpenGL как резервный вариант. Именно поэтому в UNMASKED_RENDERER_WEBGL можно увидеть строку вроде «ANGLE (NVIDIA, Direct3D11 vs_5_0 ps_5_0)». Если этот backend не соответствует заявленной операционной системе, например строка упоминает Direct3D11 при User-Agent macOS, это критическое расхождение: macOS физически не использует Direct3D. Такие несоответствия антибот-системы ловят мгновенно, ещё до анализа поведения. Строка WebGL renderer вообще имеет смысл только в паре с выводом рендера, ОС и заявленным браузером, а не как отдельное значение.
Почему AudioContext и video fingerprinting дают отдельные сигналы
OfflineAudioContext создаёт детерминированный, но не одинаковый на всех стеках сигнал. OscillatorNode генерирует волну заданной формы, DynamicsCompressorNode сжимает её амплитуду по нелинейному алгоритму, а движок браузера возвращает буфер Float32Array без какого-либо доступа к микрофону или реальному звуку: всё происходит в памяти, в режиме offline-рендеринга быстрее реального времени.
На итоговые числа влияет конкретная реализация DSP-конвейера в движке (Blink, WebKit или Gecko), архитектура CPU и набор доступных SIMD-инструкций: AVX и AVX2 на современных x86-чипах или NEON на ARM. Эти наборы инструкций позволяют обрабатывать несколько семплов одновременно, но порядок и способ суммирования floating-point значений при этом может отличаться, а именно это в итоге читается из буфера. Вместо передачи целого массива система обычно берёт агрегаты: отдельные семплы, сумму модулей сигнала, спектральные пики или квантованную последовательность значений.
Video API дают другой класс данных. MediaCapabilities.decodingInfo() возвращает объект с тремя полями, supported, smooth и powerEfficient, для конкретной комбинации кодека, разрешения и битрейта, а WebCodecs через VideoDecoder.isConfigSupported() позволяет проверить поддержку конкретного профиля H.264, VP9, AV1 или HEVC ещё до создания декодера. Аппаратное декодирование зависит от чипа и лицензий: например, поддержка HEVC на десктопе часто требует отдельного системного кодека, тогда как AV1 с аппаратным ускорением встречается лишь на более новых GPU. Отдельный класс сигналов, это поддержка Wide Color Gamut: пространства P3 и Rec.2020 можно проверить через matchMedia('(color-gamut: p3)'), и их доступность коррелирует с классом дисплея и видеокарты. Отсутствие кодека или узкого гамута не доказывает автоматизацию: причиной может быть образ управляемого устройства или политика сборки.

Энтропия не равна уникальности. Она уменьшает размер группы похожих клиентов. Редкий GPU информативен в большой популяции и почти бесполезен в парке одинаковых ноутбуков.
Энтропия Шеннона: почему отпечаток это вероятность, а не идентичность
В теории информации энтропию Шеннона считают по формуле H = -Σ p_i · log2(p_i), где p_i это вероятность конкретного значения признака среди всей популяции пользователей. Чем реже значение, тем больше бит оно добавляет к общему отпечатку. Один бит энтропии удваивает количество комбинаций: 10 бит дают примерно 1024 группы, 20 бит уже больше миллиона.
Именно поэтому ни один отдельный вектор из таблицы выше не работает как уникальный идентификатор. Canvas-хеш с 6 бит энтропии делит всех пользователей примерно на 64 группы; это полезно лишь в комбинации с другими сигналами. Реальный скоринг суммирует энтропию Canvas, WebGL, Audio, шрифтов, часового пояса и языка, и только совокупный показатель может приблизиться к практической уникальности в рамках конкретного сайта. Но даже тогда речь идёт о вероятностной кластеризации, «этот клиент с высокой вероятностью тот же, что и вчера», а не о криптографически точной идентификации. Понимание этой разницы критично и для антифрода, который не должен банить по совпадению одного редкого признака, и для приватности, где исчезновение cookie ещё не означает исчезновения возможности связать сессии.
Блокировка, шум или согласованный профиль: что видит антифрод
Полная блокировка API возвращает ошибку или пустое значение. Это ограничивает измерение, но необычный отказ сам может быть редким сигналом: в реальной популяции пользователей заблокированный WebGL встречается значительно реже, чем работающий. Noise injection меняет ответ при каждом вызове. Согласованный профиль, напротив, даёт стабильные значения, совместимые с ОС, GPU и браузером, и именно такой подход использует эмуляция железа в Afina.

| Подход | Механика работы | Что видит сайт / WAF | Риск обнаружения | Вердикт |
|---|---|---|---|---|
| блокировка вызовов / пустые данные | API возвращает ошибку, undefined или пустой буфер | отсутствие ожидаемого ответа там, где он есть у 99% реальных браузеров | высокий: само отсутствие данных это редкий и легко фильтруемый признак | не рекомендовано для прохождения серьёзного антибота |
| рандомизация шума | каждый вызов Canvas/Audio возвращает чуть другие значения | нестабильный результат при повторном измерении в рамках одной страницы | очень высокий: дисперсия выявляется статистически за несколько запросов | ещё хуже блокировки, так как добавляет новый уникальный паттерн |
| согласованная эмуляция железа (подход Afina) | параметры Canvas, WebGL, Audio и ОС согласованы между собой и стабильны между сессиями | стабильные, правдоподобные значения, соответствующие заявленному устройству | низкий при условии полной внутренней согласованности профиля | рекомендованный подход для QA и легитимного мультиаккаунтинга |
Наивный шум раскрывает статистика. Если младшие биты меняются независимо при пяти Canvas-вызовах, растут дисперсия хешей и Hamming distance. Если меняется только toDataURL, а getImageData, WebGL и CSS-вымеривания говорят другое, каналы конфликтуют. Защита может повторить вызов в рамках страницы и проверить корреляцию пикселей, и любая нестабильность там, где реальное железо стабильно, сразу повышает оценку риска.
Матрица критических расхождений: когда профиль выдаёт себя сам
Попытка подменить только один параметр, например через обычное расширение браузера, меняющее лишь navigator.platform, почти всегда делает профиль ещё заметнее: все остальные сигналы, шрифты, GPU, часовой пояс, аудиодвижок, остаются от реального железа и начинают противоречить подменённому значению. Cloudflare, DataDome, Akamai и подобные системы строят детекцию именно на поиске таких внутренних конфликтов, а не на одном «плохом» значении.
| Сигнал A | Сигнал B | Пример конфликта | Почему это триггерит детекцию |
|---|---|---|---|
| User-Agent = macOS | UNMASKED_RENDERER_WEBGL содержит «Direct3D11» | ANGLE-backend Direct3D11 физически невозможен на macOS, там используется Metal | прямое противоречие между заявленной ОС и графическим движком |
navigator.platform = Win32 | текстовый рендеринг соответствует паттерну FreeType | Windows типично использует DirectWrite, а не Linux-стек шрифтов | несоответствие подсистемы рендеринга текста заявленной ОС |
| часовой пояс Europe/Moscow, десктопный User-Agent | vendor-строка WebGL содержит мобильный чип (например Mali или Adreno) | мобильные GPU не устанавливают в десктопные системы | смешение мобильного и десктопного профиля железа |
hardwareConcurrency = 24 ядра | User-Agent описывает бюджетное мобильное устройство | в бюджетных мобильных чипах нет 24 логических ядер | нереалистичная конфигурация CPU для заявленного устройства |
MediaCapabilities подтверждает аппаратный AV1 | UNMASKED_RENDERER_WEBGL указывает на устаревшую модель GPU без блока AV1 | аппаратный AV1-декодер появился только в новых поколениях чипов | конфликт между заявленной возможностью декодирования и реальным железом |
Каждая строка таблицы показывает один и тот же принцип: изменение одного параметра без синхронного изменения всех связанных с ним сигналов создаёт конфликт, заметнее исходного значения, которое якобы нужно было скрыть.
Как проверять Chromium-профиль без JavaScript-патчей
JavaScript-патчи оставляют артефакты в дескрипторах, toString(), порядке ошибок, main frame, worker или iframe. Надёжнее настраивать механизмы Chromium, которые формируют API: графический backend, набор шрифтов, media capabilities и изолированное storage профиля.
В Afina изолированный Chromium-профиль объединяет fingerprint, proxy, cookies и cache. Сопоставляйте ОС, движок, timezone, languages, CPU, RAM и WebGL/Canvas/Audio в одном профиле, а не меняйте их после загрузки страницы. Подробнее см. управление fingerprint. Стабильность нужна между разрешёнными сессиями, пока реальное обновление не объясняет изменение.
- создайте тестовый профиль с полным набором параметров;
- выполните Canvas, WebGL, Audio и media-измерения трижды подряд;
- сравните стабильность результата в main frame, iframe и worker;
- проверьте согласованность пар сигналов по матрице выше, в частности ANGLE-backend и заявленную ОС;
- повторите тесты после обновления Chromium и зафиксируйте, какие значения изменились, а какие остались стабильными;
- задокументируйте baseline и разницу в тикет-трекере, прежде чем профиль пойдёт в работу.
Это контроль качества собственного продукта или разрешённого workflow, а не метод обхода правил сервиса.
Как приватность и антифрод должны трактовать отпечаток
Fingerprint полезен вместе с репутацией сессии, темпом запросов, сетевой согласованностью и поведением. Разумный скоринг допускает неопределённость: новый монитор или драйвер не означает захват аккаунта. Изолируйте профили, не смешивайте cookies, документируйте цель и используйте официальные API.
Как антифрод сравнивает нестабильные измерения
В реальном скоринге fingerprint не должен быть одной непрозрачной строкой. Полезнее хранить отдельные компоненты и их качество: доступен ли API, повторяется ли результат, насколько далеко новое значение от исторического кластера и не противоречит ли оно конфигурации. Например, изменение хеша Canvas после обновления GPU-драйвера выглядит иначе, чем одновременное изменение языка, экрана, шрифтов, GPU и часового пояса в течение минуты.
Скоринг часто описывают формулой risk = Σ w_i f_i, где f_i это признак, а w_i его вес. Для fingerprint лучше мыслить условными вероятностями: насколько нормальна эта комбинация для конкретного класса устройств и этого аккаунта. Вес Canvas не должен быть постоянным. Если API заблокирован политикой браузера, этот факт нужно интерпретировать отдельно, а не подставлять искусственный хеш.
Практический протокол проверки для команды
Baseline-измерения из предыдущего раздела стоит превратить в регулярную практику команды, а не разовую проверку. Зафиксируйте версию Chromium, ОС, GPU, драйвер, шрифты и результаты контрольных тестов в общем журнале, а затем воспроизводите запуск в чистом профиле, в том же профиле на следующий день и после каждого планового обновления движка. Сравнивайте новые снимки с предыдущими не вручную, а автоматизированным diff-скриптом: это даёт данные, которых не хватает большинству разовых «проверок в браузере».
Не объединяйте отпечатки пользователей с полными персональными данными без правового основания и срока хранения. Хеш тоже может быть идентификатором, если его используют для повторного распознавания. Для защитных продуктов стоит ограничивать доступ к сырым сигналам, псевдонимизировать записи и разделять обнаружение фрода и аналитику.
Где заканчивается техническая защита и начинается политика данных
Снижение fingerprinting не отменяет требований к прозрачности. Определите, какие API действительно нужны функции, когда запрашивать согласие, как объяснять его понятным языком и как удалять связанные идентификаторы. Для антифрода нужны границы: перечень допустимых сигналов, ручная проверка для высокого риска и мониторинг false positive. Регулярный аудит скриптов помогает увидеть новый API-вызов до того, как он станет незаметной зависимостью продукта, а доступ к журналам стоит ограничивать ролями и определять сроки хранения ещё до запуска функции. Команда безопасности должна проверять, не растёт ли доля оспоренных решений после каждого изменения модели или браузерного движка. Материал носит образовательный характер и не является советом по обходу проверок сервисов или challenge-страниц.
Afina хранит параметры профиля рядом с рабочими данными, а не в наборе случайных плагинов. Назначьте ответственного за проверку изменений профиля, контроль доступа и периодический просмотр собранных технических сигналов. Полезно также проверять fallback-шрифты, доступность GPU и изменения медиакодеков после каждого обновления Chromium.
СкачатьFAQ — Часто задаваемые вопросы
Что такое Canvas fingerprinting?
Canvas fingerprinting измеряет пиксели или метрики текста после Canvas-рендеринга. Различия создают шрифты, ОС, драйверы и параметры дисплея.
Очищается ли Canvas fingerprint вместе с cookies?
Нет. Очистка cookies не меняет графический стек. Результат может измениться после обновления ОС, браузера, шрифтов или драйвера.
Чем WebGL fingerprint отличается от Canvas?
WebGL добавляет данные GPU, ANGLE-backend и вывод 3D-шейдеров. Canvas чаще измеряет 2D-пиксели и растеризацию текста.
Слышен ли звук при AudioContext fingerprinting?
Обычно нет. OfflineAudioContext обрабатывает сигнал в памяти, а страница читает числовой буфер.
Почему случайный шум Canvas плохая защита?
Случайный шум делает повторные измерения нестабильными. Эта дисперсия сама становится нетипичным сигналом.
Гарантирует ли подмена fingerprint прохождение антифрода?
Нет. Антифрод оценивает много сигналов и политику конкретного сервиса. Профиль должен быть внутренне согласованным и использоваться только в разрешённых сценариях.
Что означает энтропия Шеннона в контексте fingerprinting?
Это количество бит, которое конкретный признак добавляет для различения пользователей. Больше бит означает меньшую группу похожих клиентов, а не гарантированную уникальность одного устройства.
Почему несоответствие WebGL renderer и User-Agent так легко обнаружить?
Backend ANGLE зависит от реальной ОС, поэтому строка вроде Direct3D11 при User-Agent macOS физически невозможна. Антибот-системы ищут именно такие внутренние противоречия между сигналами.
