Canvas, WebGL і Audio Fingerprinting: як браузер видає себе без жодної кукі

Canvas, WebGL, Audio та video fingerprinting вимірюють особливості рендерингу й обробки медіа, перетворюючи їх на ймовірнісний ідентифікатор браузера.
На відміну від cookie, цей сигнал не записується у сховище сторінки. Скрипт щоразу викликає звичайні веб-API, збирає відповіді та порівнює їх із попередніми візитами. Для антифроду це ознака ризику, для приватності це причина контролювати дані профілю. Відбиток браузера не є паспортом людини: після оновлення драйвера він може змінитися, але сукупність сигналів часто дозволяє пов'язати сесії.
Один Canvas-хеш нічого не доводить. Система зіставляє текстові метрики, пікселі, GPU, AudioContext, часовий пояс, мову та поведінку сесії. Якщо значення не узгоджуються між собою, сама суперечність стає індикатором, і саме на цій суперечності побудована більшість сучасних детекторів ботів.
Що вимірюють Canvas, WebGL, Audio та відео API
Fingerprinting вимірює властивості середовища, а не читає заздалегідь збережений ID. Відповіді нормалізують, об'єднують з іншими ознаками та хешують. Два однакові сценарії на різних машинах можуть видати інші байти через ОС, шрифт, драйвер або версію рушія.
Canvas fingerprinting читає пікселі чи геометрію після малювання тексту й фігур. Canvas fingerprinting чутливий до шрифтів і текстового конвеєра. WebGL додає доступні розширення, precision-формати, 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 трапляється значно рідше, ніж працюючий. Додавання шуму змінює відповідь при кожному виклику. Узгоджений профіль натомість подає сталі дані, сумісні з ОС, 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/Kyiv, десктопний 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: графічного бекенду, списку шрифтів, media capabilities та ізольованого storage профілю.
В Afina окремий Chromium-профіль поєднує fingerprint-параметри, проксі, cookies і cache. Зіставляйте ОС, браузерний рушій, timezone, languages, CPU, RAM та WebGL/Canvas/Audio-параметри в одному профілі, а не змінюйте їх після завантаження сторінки. Для цього є керування fingerprint. Профіль має залишатися сталим між легітимними сесіями, доки зміна не відображає реальну подію, наприклад оновлення браузера.
- створіть тестовий профіль з повним набором параметрів;
- виконайте Canvas, WebGL, Audio та media-вимірювання тричі поспіль;
- звірте сталість результату в main frame, iframe і worker;
- перевірте узгодженість пар сигналів за матрицею вище, зокрема ANGLE-backend і заявлену ОС;
- після оновлення Chromium повторіть тести й зафіксуйте, які значення змінилися, а які лишились сталими;
- задокументуйте baseline і різницю в тікет-трекері перед тим, як профіль піде в роботу.
Це перевірка якості для власного продукту або дозволеного workflow, а не спосіб обходити правила сервісів чи challenge-сторінки.
Як приватність і антифрод повинні трактувати відбиток
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-відбитки разом із cookie?
Ні, очищення cookie не змінює графічний стек. Відбиток може змінитися після оновлення ОС, браузера, шрифтів або драйвера.
Чим WebGL fingerprint відрізняється від Canvas?
WebGL додає дані про GPU, ANGLE-backend і результат 3D-рендерингу шейдерів. Canvas частіше вимірює 2D-пікселі та текстову растеризацію.
Чи чутно звук під час AudioContext fingerprinting?
Зазвичай ні, бо OfflineAudioContext обробляє сигнал у пам'яті. Сайт аналізує числовий буфер, а не запис із мікрофона.
Чому випадковий шум у Canvas поганий захист?
Випадковий шум робить повторні вимірювання нестабільними. Така дисперсія сама стає нетиповою ознакою профілю.
Чи гарантує підміна fingerprint проходження антифроду?
Ні, антифрод аналізує багато сигналів і політики конкретного сервісу. Профіль має бути внутрішньо узгодженим і застосовуватися лише в дозволених сценаріях.
Що означає ентропія Шеннона у контексті fingerprinting?
Це кількість бітів, яку конкретна ознака додає для розрізнення користувачів. Більше бітів означає меншу групу схожих клієнтів, а не гарантовану унікальність одного пристрою.
Чому невідповідність WebGL renderer і User-Agent так легко виявити?
Бекенд ANGLE залежить від реальної ОС, тому рядок на кшталт Direct3D11 під User-Agent macOS фізично неможливий. Антибот-системи шукають саме такі внутрішні суперечності між сигналами.
