← Назад в базу знаний

Как антифрод-системы маркетплейсов вычисляют один и тот же браузер при смене IP через прокси: canvas, WebGL и шрифты

Отпечаток начинается раньше, чем вы думаете

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

Логика простая. Пять аккаунтов с разных IP, но с одинаковым отпечатком браузера — это не пять человек. Это один человек с прокси-пулом. Антифрод-системы крупных маркетплейсов (Wildberries, Ozon, Amazon, eBay) собирают сотни параметров на каждом запросе и сравнивают их между сессиями. Совпадение по 15-20 параметрам при разных IP — почти гарантированный флаг.

Дальше разберём, что именно они смотрят. Не в общих чертах, а с конкретикой: какие API вызываются, какие байты сравниваются, где прокси не спасает.

Canvas: почему картинка одинаковая на всех машинах

Canvas fingerprinting работает так. Скрипт создаёт ``, рисует текст с градиентом и тенями, потом вызывает `toDataURL()` и хеширует результат. Пиксели зависят от GPU, драйвера, сглаживания шрифтов и операционной системы. На одинаковом железе с одинаковой ОС хеш совпадает с точностью до байта.

```javascript

const canvas = document.createElement('canvas');

canvas.width = 280;

canvas.height = 60;

const ctx = canvas.getContext('2d');

ctx.textBaseline = 'top';

ctx.font = '14px Arial';

ctx.fillStyle = '#f60';

ctx.fillRect(100, 1, 62, 20);

ctx.fillStyle = '#069';

ctx.fillText('lexic-fp-test', 2, 15);

ctx.fillStyle = 'rgba(102, 204, 0, 0.7)';

ctx.fillText('lexic-fp-test', 4, 17);

const hash = canvas.toDataURL().slice(-64);

```

Что тут важно. Даже если вы подмените User-Agent, canvas останется прежним. Антифрод увидит: IP новый, UA новый, а хеш canvas — тот же, что у аккаунта, забаненного месяц назад. Связка мгновенная.

Обход существует, но он хрупкий. Шум в пикселях (canvas poisoning) добавляет случайные значения в RGB на каждом вызове. Проблема в том, что современные антифроды вызывают `toDataURL()` несколько раз подряд и сравнивают результаты. Если хеш «пляшет» — это тоже сигнал. Стабильный шум с фиксированным seed помогает, но его нужно синхронизировать с остальными параметрами отпечатка. Иначе получится зоопарк: canvas от одной машины, WebGL от другой.

WebGL: вендор GPU как паспорт

WebGL отдаёт ещё больше. Через `WEBGL_debug_renderer_info` скрипт читает `UNMASKED_VENDOR_WEBGL` и `UNMASKED_RENDERER_WEBGL`. Типичный ответ выглядит так: `Google Inc. (NVIDIA)` и `ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 Direct3D11 vs_5_0 ps_5_0, D3D11)`. Это фактически модель вашей видеокарты и версия драйвера.

```javascript

const gl = document.createElement('canvas').getContext('webgl');

const ext = gl.getExtension('WEBGL_debug_renderer_info');

const vendor = gl.getParameter(ext.UNMASKED_VENDOR_WEBGL);

const renderer = gl.getParameter(ext.UNMASKED_RENDERER_WEBGL);

console.log(vendor, renderer);

```

Дальше антифрод сравнивает рендерер с платформой. Если UA говорит «Android на телефоне», а WebGL отдаёт дискретную NVIDIA — это несоответствие. Если UA говорит «Windows 10», а рендерер отдаёт `Apple M1` — тоже. Такие пары ловятся на раз-два.

Есть ещё один слой — параметры рендеринга. `MAX_TEXTURE_SIZE`, `MAX_VIEWPORT_DIMS`, `ALIASED_LINE_WIDTH_RANGE`. Они зависят от драйвера и GPU. Совокупность этих значений формирует стабильный хеш. Подменить его сложнее, чем UA: нужно эмулировать целый GPU на уровне JS-контекста.

Шрифты и метрики текста: невидимый почерк

Метрики шрифтов — недооценённый сигнал. Скрипт измеряет ширину и высоту отрендеренного текста в пикселях для десятка шрифтов и размеров. Разные версии Windows и разные наборы шрифтов дают разные значения. Например, ширина строки «mmmmmmmmmmlli» в Arial 72px на Windows 10 — 723.4px, на Windows 11 с обновлённым Arial — 723.9px. Мелочь, но она стабильна.

```javascript

function measure(font) {

const span = document.createElement('span');

span.style.cssText = `font:72px \${font};position:absolute;visibility:hidden`;

span.textContent = 'mmmmmmmmmmlli';

document.body.appendChild(span);

const w = span.offsetWidth;

span.remove();

return w;

}

const fp = ['Arial', 'Verdana', 'Times New Roman', 'Courier New']

.map(f => `\${f}:\${measure(f)}`).join('|');

```

Маркетплейсы добавляют к этому список установленных шрифтов через `document.fonts.check()`. Если у двух аккаунтов совпадает набор из 40 шрифтов и метрики до десятых пикселя — это одна машина. Смена IP тут ничего не меняет.

Полный список того, что летит в заголовки и JS

Антифрод собирает данные в два потока. Первый — HTTP-заголовки: `User-Agent`, `Accept-Language`, `Accept-Encoding`, порядок заголовков, `Sec-CH-UA` (Client Hints). Второй — JS-зонд, который выполняется на странице. Он читает `navigator`, `screen`, `Intl`, `AudioContext`, `WebRTC`.

Вот таблица реальных параметров, которые сравнивает типичная антифрод-система маркетплейса:

| Параметр | Источник | Стабильность | Легко подменить |

|---|---|---|---|

| Canvas hash | JS | Высокая | Нет |

| WebGL renderer | JS | Высокая | Частично |

| AudioContext hash | JS | Средняя | Нет |

| Список шрифтов | JS | Высокая | Нет |

| Часовой пояс | Intl | Высокая | Да |

| Разрешение экрана | screen | Средняя | Да |

| `navigator.hardwareConcurrency` | JS | Средняя | Да |

| Порядок HTTP-заголовков | Сеть | Высокая | Только на уровне клиента |

| TLS-отпечаток (JA3) | Сеть | Высокая | Только кастомный TLS-стек |

Последняя строка — самое больное. JA3/JA4 хеширует параметры TLS handshake: версию, набор шифров, расширения, порядок. Chrome и Firefox дают разные JA3. Прокси на уровне TCP этого не меняет — TLS-рукопожатие идёт от вашего браузера. Если вы используете `curl` с TLS-стеком OpenSSL, а UA подставляете от Chrome — антифрод увидит несоответствие за миллисекунды.

Где прокси ломается: реальные кейсы

Первый кейс. Сервер на nginx 1.24, настроен как reverse proxy для пула аккаунтов. Клиенты ходят через `proxy_pass` с разными исходящими IP. Проблема: все запросы приходят с одинаковым `Accept-Language: ru-RU,ru;q=0.9` и одинаковым порядком заголовков, потому что nginx пересобирает их по своему шаблону. Антифрод маркетплейса смотрел на порядок заголовков и на отсутствие `Sec-CH-UA-Mobile`. Пять аккаунтов с разных IP получили бан за неделю. Причина — не IP, а идентичный HTTP-профиль. Решение: проброс заголовков клиента без пересборки (`proxy_pass_request_headers on`, отключение `proxy_set_header` для всех полей, кроме `Host`), плюс разные профили браузеров на клиентах.

Второй кейс. Python-скрипт на `requests` с ротацией прокси из пула. 200 запросов в час, каждый с нового IP. Через два дня — блокировка всех аккаунтов. Причина: `requests` использует TLS-стек OpenSSL с фиксированным набором шифров. JA3-хеш одинаковый для всех запросов. Плюс `requests` не выполняет JS, поэтому canvas/WebGL отсутствуют — а их отсутствие на «обычном» браузерном трафике выглядит подозрительно. Решение: `curl_cffi` с имперсонацией Chrome, либо полноценный headless-браузер с профилями.

Третий кейс. Ферма виртуальных машин на одном гипервизоре. Каждая VM — с отдельным IP через прокси-пул. Антифрод заметил: `AudioContext` fingerprint совпадает у 30 аккаунтов. Причина — одинаковый виртуальный аудиодрайвер (эмуляция Intel HDA от гипервизора). Звуковой отпечаток зависит от драйвера и буферизации. На одинаковых VM он идентичен. Решение — раскидать по разным типам железа или использовать браузерные профили с шумом в AudioContext.

Что делать: практический минимум

Если задача — не ферма, а несколько аккаунтов с разных IP, начните с консистентности. Отпечаток должен быть цельным: UA от Chrome, canvas от той же машины, WebGL от того же GPU, часовой пояс от того же региона, что и IP. Прокси с IP в Германии и `Accept-Language: ru-RU` — красный флаг.

```bash

curl -x http://user:pass@proxy.example:8080 \

-H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36" \

-H "Accept-Language: de-DE,de;q=0.9" \

-H "Sec-CH-UA: \"Chromium\";v=\"124\", \"Google Chrome\";v=\"124\"" \

--tlsv1.3 \

https://example.com/api/health

```

Флаг `--tlsv1.3` не решит проблему JA3, но хотя бы приведёт версию протокола к браузерной. Для реальной маскировки нужен клиент, который воспроизводит весь TLS-отпечаток. В экосистеме для этого есть `curl_cffi` (Python) и `tls-client`. Они эмулируют JA3 Chrome/Firefox/Safari на уровне рукопожатия.

Отдельно про IPv6. Многие антифроды хуже покрывают IPv6-диапазоны, потому что исторически их меньше. Прокси-сервисы вроде lexic.ml выдают IPv6-адреса с /64-подсетью — это даёт большой пул адресов для ротации без переиспользования. Но помните: смена IP внутри /64 не меняет отпечаток браузера. Canvas, WebGL и шрифты останутся теми же, и антифрод склеит аккаунты по ним, а не по адресу.

Итог: IP — это 10% задачи

Антифрод маркетплейсов давно сместил фокус с сетевого уровня на клиентский. Canvas, WebGL, шрифты, AudioContext, TLS-отпечаток — вот что сравнивается в первую очередь. Прокси решает задачу «разные IP», но не решает задачу «разные машины». Если отпечаток браузера одинаковый, смена IP только добавляет подозрений: зачем один человек меняет адрес десять раз в час.

Рабочая схема — консистентный профиль на каждый аккаунт. Отдельный браузерный профиль с уникальными canvas/WebGL/audio-параметрами, TLS-стек, соответствующий UA, язык и часовой пояс под регион IP. И тогда прокси — включая IPv6-пулы — работает как задумано: разносит трафик, не создавая новых связей между аккаунтами.

✔️Купить прокси