Как сайты отличают V8, SpiderMonkey и JavaScriptCore: стек вызовов, тексты ошибок, toString и точность Math — и что этот сигнал способен доказать.
Строка User-Agent — это лишь заявление. Код страницы на самом деле выполняет движок JavaScript, и его особенности реализации проявляются через обычные стандартные API. Поэтому сигналы движка полезны не как высокоэнтропийный идентификатор, который следует за вами по сайтам, а как детектор лжи для той личности, которую браузер о себе заявляет. Если нужен фон о том, что такое движок JavaScript и как он сочетается с движком рендеринга, начните со статьи Движок рендеринга vs движок JavaScript; здесь мы сразу переходим к приёмам.
Ключевые выводы
- Три движка покрывают почти все реальные браузеры: V8 (Chrome, Edge и другие браузеры на Chromium), SpiderMonkey (Firefox) и JavaScriptCore (Safari и на практике почти все браузеры на iOS).
- Отличия берутся из мест, которые спецификация языка оставляет открытыми: формат стека вызовов, формулировки встроенных ошибок, текст исходников нативных функций и точность некоторых функций
Math. - Сигнал грубый: он раскладывает посетителей на несколько корзин (плюс различия между версиями), поэтому сам по себе определяет движок, а не человека.
- Его реальная ценность — проверка согласованности: браузер, который называет себя Safari, но выполняется как V8, рассказал странице две разные истории.
- Если другие признаки движка неубедительны, собственная проверка согласованности BrowserInsight определяет его по формату
Error().stack.
Почему движок выдаёт себя
Спецификация ECMAScript определяет, что язык обязан делать, но не то, как должна выглядеть каждая наблюдаемая деталь. Там, где спецификация молчит или прямо оставляет решение реализации, каждый движок сделал свой выбор, и эти выборы годами остаются стабильными, потому что сайты стали на них полагаться. Особые разрешения не нужны: скрипт читает строку, сравнивает её с тем, что выдаёт каждый движок, и получает ответ.
Приём 1. Форма Error.stack
Error.prototype.stack не входит в стандарт, поэтому и различается. V8 начинает строку с имени и сообщения ошибки, а кадры пишет как at functionName (url:line:column). SpiderMonkey и JavaScriptCore не добавляют такой заголовок и пишут functionName@url:line:column. Два не-V8 движка различаются между собой более мелкими деталями — записью анонимных кадров и номеров столбцов; эти детали зависят от версии, их нужно перепроверять, а не зашивать в код.
V8 также ввёл два сопутствующих API: Error.captureStackTrace и Error.stackTraceLimit. MDN помечает captureStackTrace как нестандартный, а его таблица совместимости показывает, какие движки добавили его позже. Наличие или отсутствие таких V8-расширений — вторая независимая подсказка.
Приём 2. Текст встроенных сообщений об ошибках
Спровоцируйте одну и ту же ошибку в трёх движках (вызов не-функции, чтение свойства у null) — получите три разные фразы, потому что спецификация задаёт тип ошибки, но не сообщение. Справочник MDN по ошибкам JavaScript перечисляет множество ошибок с вариантами сообщений по движкам. Страница может перехватить исключение, прочитать error.message и сопоставить формулировку с небольшой таблицей. Тексты меняются и между версиями одного движка, поэтому аккуратная проверка считает их свидетельством о семействе движка, а не о точной версии.
Приём 3. Function.prototype.toString для нативных функций
Вызов Function.prototype.toString для встроенной функции вроде Array.prototype.push возвращает текст вида function push() { [native code] }. Современные спецификации фиксируют общий вид, но оставляют простор для пробелов и форматирования, и сама строка и её длина исторически различались между движками. Скрипт может сравнить результат для нескольких встроенных функций с тем, что должен выдавать заявленный браузер. У этой пробы есть и второе применение: функция, подменённая JavaScript-обёрткой, перестаёт печатать [native code], поэтому на неё опираются инструменты разоблачения подмен.
Приём 4. Точность Math на краях
Спецификация ECMAScript требует лишь, чтобы Math.sin, Math.exp, Math.pow и подобные возвращали приближённый реализацией результат. Справочник MDN по Math отмечает, что точность многих из этих функций зависит от реализации. Поэтому движки (а иногда и математические библиотеки CPU или ОС) могут расходиться в последних разрядах при необычных аргументах. Поскольку такое поведение зависит и от математической библиотеки, и от движка, считайте его вспомогательным сигналом для кластеризации посетителей, а не чистой меткой «по одной на движок». Конкретные цифры мы не приводим: они меняются с версиями и платформами.
Приём 5. Пределы рекурсии
Запустите рекурсию до отказа движка и перехватите то, что он выбросит. V8 и JavaScriptCore выбрасывают RangeError с сообщением о превышении максимального размера стека вызовов; SpiderMonkey — свой нестандартный InternalError с сообщением «too much recursion» (см. статью MDN too much recursion). И тип ошибки, и формулировка — грубые признаки движка. А глубина, достигнутая до ошибки, зависит от размера кадра, размера стека и устройства, поэтому это скорее намёк на сборку и окружение, чем точный идентификатор движка, и это самая нестабильная проба в списке.
Почему это трудно подделать
Антидетект-браузер на основе Chromium может переписать User-Agent, Client Hints и свойства navigator, но он по-прежнему работает на V8. Чтобы V8 выдавал формат стека, формулировки ошибок, текст toString и результаты Math в стиле SpiderMonkey или JavaScriptCore, нужно править множество независимых поведений, а каждая исправленная встроенная функция сама становится местом, где изменение на уровне прототипа может проявиться. Поэтому проверки движка хорошо сочетаются с логикой анализа в стиле CreepJS, а разрыв между подправленным профилем и настоящим браузером (см. антидетект-браузеры и настоящие браузеры) обычно находят в несогласованности сигналов между собой, а не в одном поле. Эта статья описывает обнаружение, а не способ его обхода.
Где это применяется
Законное применение — проверка согласованности. Системы защиты от ботов и мошенничества сравнивают браузер, которым сессия называет себя (см. как обнаруживают подмену User-Agent), с движком, который реально выполняет код, — как один из многих входов в детекции ботов и детекции headless-браузеров. Та же идея работает грубее для диапазонов версий: определение версии по возможностям показывает, насколько новый движок, а приёмы выше — какой именно это движок.
Что на самом деле проверяет BrowserInsight
Скажем прямо: проверяются две вещи. Проверка согласованности, на которой построены /fingerprint-check, /bot-detection и /vpn-check, если другие сигналы неубедительны, определяет движок по формату Error().stack. Проверка ядра отдельно оценивает версию движка по поддержке возможностей. Мы не проверяем формулировки ошибок, точность Math и длину toString, и всё работает в вашем браузере — на сервер ничего не отправляется.
Часто задаваемые вопросы
Может ли сайт опознать меня по движку JavaScript? Сам по себе — нет. Движок делит посетителей на несколько больших групп и почти ничего не добавляет к уникальности отпечатка. Его ценность — в проверке, согласуются ли остальные ваши заявления.
Означает ли другой движок JavaScript другой браузер? Обычно да. Chrome, Edge и Brave работают на V8, Firefox — на SpiderMonkey, Safari и почти все браузеры iOS — на JavaScriptCore. Браузеры одного семейства по движку не различить.
Эти различия постоянны? Нет. Движки меняют формулировки, форматы стека и математические реализации между релизами, а работа над стандартами может убрать часть различий. Любая детекция на них требует сопровождения.
Error.stack — это стандарт?
Он широко реализован, но не стандартизирован, как фиксирует MDN, — поэтому его формат и служит сигналом движка.
Заключение
Фингерпринтинг движка JavaScript — небольшой и честный сигнал: несколько корзин-движков, считанных по стекам вызовов, текстам ошибок, исходникам нативных функций, результатам Math и пределам рекурсии. Он не выделит вас из толпы, но покажет, что описание браузера и его поведение расходятся. Чтобы увидеть свой движок глазами страницы, запустите проверку отпечатка или проверку ядра.

