Вокруг браузерных отпечатков сложилось устойчивое заблуждение, которое кочует из статьи в статью. Звучит оно примерно так: чем уникальнее отпечаток, тем труднее связать сессии между собой, значит, надо максимально рандомизировать параметры.
Логика выглядит убедительно ровно до того момента, пока не посмотришь, что именно считает принимающая сторона. Антифрод почти никогда не спрашивает «встречался ли нам раньше такой набор параметров». Он спрашивает другое: «бывают ли вообще такие устройства». Это два разных вопроса, и ответы на них расходятся в противоположные стороны.
Разберу, откуда берётся расхождение, покажу, как оно считается, и почему рандомизация параметров чаще ухудшает результат, чем улучшает.
Немного энтропии
Начнём с того, как вообще измеряют информативность отдельного параметра.
Если доля пользователей с некоторым значением равна p, то наблюдение этого значения даёт наблюдателю −log₂§ бит информации. Величина называется собственной информацией, или неожиданностью наблюдения. Чем реже значение, тем больше бит оно приносит.
Классический эксперимент здесь — Panopticlick от EFF, опубликованный в 2010 году. На выборке из сотен тысяч браузеров средний отпечаток нёс около 18,1 бита энтропии, а уникальными оказались 83,6% браузеров, в конфигурациях с Flash и Java — 94,2%. Данные сильно устарели, набор доступных API с тех пор изменился радикально, но сама методика измерения осталась той же.
Посчитать вклад набора параметров можно так:
import math from collections import Counter def surprisal_bits(value, distribution): """Сколько бит даёт наблюдение конкретного значения. distribution — словарь {значение: доля пользователей}. Отсутствующее значение считаем крайне редким, а не невозможным: иначе получим деление на ноль там, где на деле просто мало данных. """ p = distribution.get(value, 1e-6) return -math.log2(p) def dataset_entropy(values): """Энтропия Шеннона по эмпирической выборке значений параметра.""" counts = Counter(values) total = sum(counts.values()) return -sum((c / total) * math.log2(c / total) for c in counts.values())
Функция surprisal_bits отвечает на вопрос «насколько редок конкретный пользователь», а dataset_entropy — на вопрос «насколько вообще информативен этот параметр по популяции». Первое считают для одной сессии, второе — при отборе признаков для модели.
Практический смысл в том, что 33 бита достаточно, чтобы выделить одного человека из примерно восьми миллиардов. Набор из десятка параметров средней редкости этот порог перекрывает с запасом. Именно отсюда растёт вывод «надо рандомизировать», и именно здесь он ломается.
Где ломается логика уникальности
Рандомизация действительно повышает энтропию наблюдения. Проблема в том, что она повышает её слишком сильно.
Представьте набор: видеокарта верхнего сегмента, разрешение экрана 1366×768, два гигабайта оперативной памяти, восемь логических ядер, набор шрифтов Windows и User‑Agent, заявляющий macOS. Каждый параметр по отдельности встречается в природе. Их сочетание не встречается никогда.
Наблюдатель считает не частоту набора целиком — таких данных у него и нет. Он считает условные вероятности между параметрами. Вопрос формулируется как P(разрешение | видеокарта), P(шрифты | платформа), P(память | число ядер). Если хотя бы одно из этих произведений уходит в околонулевую область, набор помечается как несуществующая конфигурация. И это гораздо более сильный сигнал, чем редкость.
Грубая модель правдоподобия:
def plausibility(profile, conditional_probs): """Оценка правдоподобия набора через произведение условных вероятностей. conditional_probs — словарь вида {('platform', 'fonts'): {('Windows', 'win_set'): 0.94, ('macOS', 'win_set'): 0.001}} Работаем в логарифмах: произведение десятка вероятностей быстро уходит за пределы точности float. """ log_score = 0.0 for (key_a, key_b), table in conditional_probs.items(): pair = (profile[key_a], profile[key_b]) p = table.get(pair, 1e-6) log_score += math.log(p) return log_score
Чем ниже log_score, тем меньше вероятность, что такое устройство существует. Уникальность здесь не участвует вообще. Обычный офисный ноутбук с полностью типовым набором параметров получит высокий балл правдоподобия, хотя его энтропия близка к минимальной.
Отсюда контринтуитивный вывод: наблюдателю выгодно ловить не редкие конфигурации, а невозможные. Первых много и среди честных пользователей, вторых в природе не бывает.
Пары, которые проверяются в первую очередь
На практике полноценная вероятностная модель нужна редко. Большинство противоречий ловится прямым сравнением.
Часовой пояс и геолокация IP. Самая дешёвая проверка из существующих. Intl.DateTimeFormat().resolvedOptions().timeZone возвращает зону IANA, IP резолвится по базе GeoIP на стороне сервера. Немецкий адрес и Europe/Moscow — расхождение, которое ловится одним сравнением.
Язык, локаль и география. Здесь нужна осторожность: эмигранты, экспаты и просто люди с английской системой существуют. Одиночное расхождение почти ничего не значит. Значимым оно становится на выборке: если полсотни сессий с немецких адресов дают одинаковую нетипичную локаль, это уже не совпадение.
User‑Agent и реальная платформа. navigator.userAgent заявляет одно, а navigator.platform, набор доступных шрифтов, поведение navigator.hardwareConcurrency и особенности отрисовки говорят другое.
Разрешение экрана и доступная область. screen.height всегда больше window.innerHeight на высоту интерфейса браузера, а screen.availHeight меньше screen.height на высоту панели задач. Точное равенство — признак headless‑среды или грубой подмены.
WebGL‑рендерер и класс устройства. Строка рендерера из WEBGL_debug_renderer_info сопоставляется с разрешением, числом ядер и объёмом памяти. Профессиональная карта рядом с параметрами бюджетного ноутбука — типичный артефакт подмены одного поля без остальных.
Простой сборщик сигналов, из которого видно, насколько мало для этого нужно:
function collectConsistencySignals() { const gl = document.createElement('canvas').getContext('webgl'); const dbg = gl && gl.getExtension('WEBGL_debug_renderer_info'); return { timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, locale: Intl.DateTimeFormat().resolvedOptions().locale, languages: navigator.languages, platform: navigator.platform, cores: navigator.hardwareConcurrency, memory: navigator.deviceMemory, screen: [screen.width, screen.height], avail: [screen.availWidth, screen.availHeight], inner: [window.innerWidth, window.innerHeight], dpr: window.devicePixelRatio, renderer: dbg ? gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : null, }; }
Ничего экзотического: все вызовы доступны любому скрипту на странице без разрешений пользователя. Дальше значения уезжают на сервер, где к ним добавляется геолокация IP и сравнение с эталонными распределениями.
Проверка расхождений выглядит примерно так:
function findContradictions(sig, ipCountry) { const problems = []; // Экран физически не может быть меньше окна. if (sig.screen[1] < sig.inner[1]) { problems.push('window_taller_than_screen'); } // Полное совпадение экрана и рабочей области: // ни панели задач, ни интерфейса браузера. if (sig.screen[0] === sig.avail[0] && sig.screen[1] === sig.avail[1]) { problems.push('no_chrome_no_taskbar'); } // Заявленная платформа против набора признаков. const claimsMac = /Mac/i.test(sig.platform); const rendererLooksWindows = /Direct3D|ANGLE/i.test(sig.renderer || ''); if (claimsMac && rendererLooksWindows) { problems.push('platform_renderer_mismatch'); } // Часовой пояс против страны IP — сравнение по таблице соответствий. if (ipCountry && !timezoneFitsCountry(sig.timezone, ipCountry)) { problems.push('timezone_geo_mismatch'); } return problems; }
Каждая проверка тривиальна, и в этом суть. Дорогие вероятностные модели нужны на следующем этапе, а первичный отсев делается арифметикой, которую можно написать за вечер.
Почему шум вредит
Многие инструменты подмены предлагают режим случайного шума: значения Canvas, WebGL и AudioContext слегка меняются от запуска к запуску. Идея в том, что стабильный отпечаток можно отследить, а плавающий нельзя.
На одной сессии это работает. На дистанции появляется новая проблема.
Реальное устройство даёт стабильный результат отрисовки. Одна и та же видеокарта с одним и тем же драйвером рисует одну и ту же скрытую картинку одинаково — на этом фингерпринтинг Canvas и построен. Если при каждом заходе результат отличается, наблюдатель видит устройство, которое меняет видеокарту несколько раз в день.
То есть нестабильность сама становится признаком. Причём признаком узким: доля честных пользователей, у которых Canvas плавает между сессиями, невелика, и объясняется она в основном обновлениями драйверов и браузера, то есть редкими событиями, а не каждым запуском.
Отсюда практическое правило, которое противоречит настройкам по умолчанию во многих инструментах: для долгоживущих сессий стабильная подмена лучше случайной. Шум оправдан только там, где сессия одноразовая и связывать её не с чем.
Версия ядра как отдельный сигнал
Параметр, который редко обсуждают, хотя он влияет на все профили сразу.
Браузеры обновляются автоматически и молча. Из‑за этого доля пользователей на версии двухлетней давности в реальном трафике исчезающе мала — основную массу составляют свежие релизы и небольшой хвост корпоративных машин с политиками обновления.
Значит, устаревшая версия ядра сама по себе даёт высокую собственную информацию, то есть работает ровно против той цели, ради которой подмену затевали. Инструмент, отстающий от актуального стабильного релиза на десятки версий, помечает каждую свою сессию как редкую конфигурацию ещё до того, как что‑то произойдёт.
Проверяется это тривиально: сравнить версию в User‑Agent с текущим стабильным релизом. Разрыв в одну‑две версии нормален, разрыв в год — нет.
Ограничения такого подхода
Честности ради, у модели согласованности есть слабые места, и они существенные.
Первое: она даёт ложные срабатывания на честных пользователях. Человек с корпоративным ноутбуком, экзотической локалью и VPN компании выглядит подозрительно по всем перечисленным критериям. Любая система, построенная на этих сигналах, обязана работать в связке с другими факторами, а не выносить решение самостоятельно.
Второе: распределения параметров нужно откуда‑то брать и постоянно обновлять. Модель, обученная на трафике двухлетней давности, будет считать нормальными конфигурации, которых уже нет, и подозрительными те, что стали массовыми.
Третье, и самое важное: технический отпечаток вообще не главный сигнал. Сетевые характеристики, поведенческие паттерны, тайминги действий и платёжные атрибуты дают на практике больше, чем весь набор параметров браузера вместе взятый. Отпечаток удобен тем, что снимается дёшево и мгновенно, а не тем, что он самый информативный.
Что из этого следует
Формулировка задачи «сделать отпечаток уникальным» неверна на уровне постановки. Уникальность — это метрика наблюдателя, а не защищающейся стороны, и максимизировать её означает работать против себя.
Осмысленная формулировка звучит иначе: набор параметров должен принадлежать классу устройств, который существует и массово встречается. Скучный типовой ноутбук со стабильными между сессиями значениями и внутренне непротиворечивыми параметрами решает задачу лучше, чем тщательно рандомизированный уникум.
Проверять себя стоит не по единственному числу «уникальность отпечатка», которое показывают публичные чекеры, а по списку противоречий. Их немного, и почти все ловятся сравнением пар значений, которое несложно написать самостоятельно.