BotD: какие детекторы действительно выполняются
Прочитайте закрепленный реестр FingerprintJS BotD и запустите его детекторы на явных состояниях компонентов, чтобы увидеть, какие проверки срабатывают и какие молчат.
BotBrowser Team
Нужна структурированная документация по теме Отпечатки?
Эта статья относится к редакционной библиотеке. Для пошаговой настройки, справки и постоянных обновлений переходите сразу в соответствующий раздел docs.
BotBrowser поддерживает повтор разрешенного fixture на том же origin под объявленным профилем и сохраняет ограниченные наблюдения о том, какие поверхности автоматизации страница может прочитать; он не может запускать сторонний сервис детектирования, решать вердикт поставщика или обещать, что очистка одной поверхности очистит остальные. Этот разбор следует публичному исходнику FingerprintJS BotD на commit 193c471ac70530afcebe5b09664c09f611d19218, читает оба файла реестра и затем выполняет собственные функции детекторов на явных состояниях компонентов, чтобы утверждения ниже опирались на наблюдаемый вывод, а не только на чтение кода.
TL;DR
- BotD собирает 23 именованных источника и предоставляет 18 детекторов; один файл детектора в закрепленном дереве экспортируется, но не регистрируется, поэтому его ветвь не может дать вердикт.
- Вердикт является сверткой по детекторам, а не голосованием: последний детектор, назвавший бота, заменяет предыдущий, поэтому названный тип зависит от порядка реестра.
- Компонент, чей источник бросил исключение, сохраняет состояние без успеха, и каждый детектор проверяет это состояние, поэтому отсутствующее значение обычно дает отсутствие вердикта, а не догадку.
- Запуск закрепленных функций на синтетических состояниях показывает поверхность, которая очищается, пока другая все еще сообщает автоматизацию, две точные ветви ложноотрицательного результата и результат
trueбез имени, теряющий имя детектора. - Наблюдаемое поведение принадлежит этому commit, а не всем детекторам и тем более не решению размещенного сервиса.
Contents
- Что на самом деле содержит закрепленный реестр
- Запуск детекторов на явных состояниях компонентов
- Где очистка одной поверхности оставляет другой сигнал
- Какие проверки пропускают признак при отсутствующем значении
- Какие ветви сводят вердикт к одному движку или значению
- Как сделать запуск воспроизводимым по записи
- Как читать вердикт без преувеличения
Что на самом деле содержит закрепленный реестр
src/sources/index.ts регистрирует 23 функции источника: android, browserKind, browserEngineKind, documentFocus, userAgent, appVersion, rtt, windowSize, pluginsLength, pluginsArray, errorTrace, productSub, windowExternal, mimeTypesConsistent, evalLength, webGL, webDriver, languages, notificationPermissions, documentElementKeys, functionBind, process и distinctiveProps. Каждый источник это простая функция, возвращающая значение или бросающая ошибку. Источник, который не может прочитать свою поверхность, бросает BotdError с состоянием State, а collect() в src/api.ts превращает такую ошибку в компонент { state, error } вместо значения.
src/detectors/index.ts регистрирует 18 функций детекторов в фиксированном порядке объявления: detectAppVersion, detectDocumentAttributes, detectErrorTrace, detectEvalLengthInconsistency, detectFunctionBind, detectLanguagesLengthInconsistency, detectNotificationPermissions, detectPluginsArray, detectPluginsLengthInconsistency, detectProcess, detectUserAgent, detectWebDriver, detectWebGL, detectWindowExternal, detectWindowSize, detectMimeTypesConsistent, detectProductSub и detectDistinctiveProperties.
В каталоге детекторов лежит 19 файлов. src/detectors/rtt.ts экспортирует detectRTT, и detectRTT не встречается в этом объекте реестра. Сравнение ключей реестра с экспортируемыми именами той же выгрузки дает 19 файлов и 18 зарегистрированных имен, и rtt.ts оказывается единственным файлом, чей экспорт никогда не достигается. Это факт кода одного commit; он ничего не говорит о следующей версии и ничего не говорит о размещенном сервисе, который мог бы импортировать функцию напрямую.
Свертка в src/api.ts решает, как получается единственный вердикт. detect() проходит по detectors в порядке объявления, вызывает каждый с словарем компонентов и присваивает finalDetection всякий раз, когда детектор сообщает бота. Детектор, вернувший строку, записывает ее как botKind; детектор, вернувший просто true, записывает BotKind.Unknown. Поскольку присваивание побеждает накопление, названный тип принадлежит последнему сработавшему детектору реестра, а не самому точному или самому уверенному. collect() запускает источники параллельно через Promise.all, поэтому словарь готов до того, как его увидит любой детектор, и только порядок детекторов решает тип.
Запуск детекторов на явных состояниях компонентов
Словарь компонентов является единственным входом каждого детектора, поэтому закрепленные функции можно выполнить на состояниях, написанных вручную, без браузера, профиля и сетевого запроса. Команда ниже собирает закрепленное дерево через esbuild и вызывает экспортируемые детекторы, печатая по одной строке на случай:
npx esbuild src/detectors/index.ts src/api.ts \
--bundle --format=cjs --platform=node --outdir=/tmp/botd-build
node /tmp/botd-probe.mjs
Проба задает базовый словарь с android: false, browserKind: 'chrome', browserEngineKind: 'chromium' и documentFocus: true, затем подменяет один или два компонента в каждом случае. Успешный компонент несет { value, state: 0 }, а нечитаемый несет { state: -1, error }, в соответствии с перечислением State в src/types.ts (Success = 0, Undefined = -1, UnexpectedBehaviour = -3). Наблюдаемый вывод закрепленных функций:
case 1 [detectWebDriver] bot: headless_chrome - driven browser: webdriver flag true
case 2 [detectDistinctiveProperties] bot: webdriver - sanitized flag but webdriver-shaped window property present
case 3 [detectDistinctiveProperties] bot: false - flag layer removed only, bridge gone too
case 4 [detectWindowSize] bot: headless_chrome - headless default window 0x0 while document has focus
case 5 [detectWindowSize] bot: false - headless report 0x0 but tab never focused
case 6 [detectPluginsArray] bot: headless_chrome - empty plugin list
case 7 [detectRTT] not-runnable: not registered in detectors/index.ts - connection rtt reported as zero
case 8 [detectWebDriver] bot: false - webdriver source unreadable instead of false
Восемь случаев выше решают четыре небольшие функции, и их стоит читать напрямую, а не выводить из вывода пробы. src/detectors/webdriver.ts состоит из одной строки: он возвращает BotKind.HeadlessChrome только при webDriver.state === State.Success && webDriver.value. src/detectors/window_size.ts сначала проверяет, что windowSize и documentFocus в State.Success, выходит раньше, если documentFocus.value ложно, и только затем сравнивает outerWidth и outerHeight с нулем. src/detectors/plugins_array.ts возвращает BotKind.HeadlessChrome при pluginsArray.state === State.Success && !pluginsArray.value. src/detectors/distinctive_properties.ts проходит по возвращенному объекту и возвращает первое имя бота, чей флаг истинен.
Та же дисциплина чтения работает, когда один детектор очищается, а другой нет. Для стороны сбора того же поставщика сбор данных и происхождение в FingerprintJS OSS показывает, как компоненты превращаются в сводное значение, а обнаружение и предотвращение автоматизации перечисляет более широкий набор поверхностей, которые страница может проверить, поэтому вердикт одной функции никогда не является полной картиной.
Где очистка одной поверхности оставляет другой сигнал
Случаи с 1 по 3 являются самым ясным результатом запуска. При webDriver в успешном true детектор detectWebDriver сообщает headless_chrome. При webDriver в успешном false, но distinctiveProps с { webdriver: true }, детектор detectWebDriver ничего не сообщает, а detectDistinctiveProperties вместо этого сообщает webdriver. При обеих чистых поверхностях detectDistinctiveProperties ничего не сообщает, и случай сводится к bot: false.
Отсюда следуют два вывода, и оба важны для того, кто цитирует вердикт этой библиотеки. Проверки независимы: очистка флага не очищает список свойств, потому что список читается через Object.getOwnPropertyNames(window) и Object.getOwnPropertyNames(document) внутри src/sources/distinctive_properties.ts, а не из флага. И названный тип меняется вместе с поверхностью, которая уцелела: headless_chrome и webdriver это разные метки, порожденные одной библиотекой из одного словаря, поэтому отчет, цитирующий одну при существовании другой, цитирует порядок реестра, а не набор присутствующих сигналов.
Список свойств достаточно конкретен, чтобы проверять его вручную. Он охватывает 14 типов ботов и перечисляет 26 элементов window (один из них шаблон, а не буквальное имя) и 17 элементов document, от awesomium и RunPerfTest на стороне CEF до callPhantom и _phantom для PhantomJS. Одна только группа WebDriver несет webdriver, __webdriverFunc, __lastWatirAlert, _WEBDRIVER_ELEM_CACHE и ChromeDriverw на стороне window, и __webdriver_script_fn, __driver_evaluate, __fxdriver_unwrapped, $cdc_asdjflasutopfhvcZLmcfl_ и $chrome_asyncScriptInfo на стороне document. Разбор, который смотрит только window, пропустит группу document, а разбор, который смотрит только флаг, пропустит обе.
Какие проверки пропускают признак при отсутствующем значении
Случай 8 показывает первую ветвь ложноотрицательного результата. Когда компонент webDriver находится в State.Undefined, detectWebDriver возвращает false, потому что защитная проверка требует State.Success до чтения значения. Нечитаемый navigator.webdriver, например страница, скрывающая свойство, realm, где чтение бросает ошибку, или обертка, не возвращающая ничего, поэтому записывается как отсутствие признака автоматизации, а не как положительный сигнал. Это разумный выбор для библиотеки, предпочитающей молчание догадке, но он означает, что отсутствующее значение и чистое значение в вердикте неразличимы.
Случай 5 показывает вторую ветвь ложноотрицательного результата, и ее легче упустить, потому что компоненты присутствуют и корректны. src/detectors/window_size.ts выходит раньше, когда documentFocus.value ложно, до того как выполняется сравнение с нулевым размером. Безголовое окно с outerWidth и outerHeight равными нулю помечается только пока документ в фокусе. Вкладка, открытая в фоне, или снимок, который выполняется до прихода фокуса, дают те же измерения и никакого вердикта. Комментарий в коде объясняет намерение, что новая фоновая вкладка тоже сообщает нулевой внешний размер, поэтому ранний выход намеренный, но следствие для читателя в том, что одни и те же числа ведут к разным вердиктам в зависимости от момента фокуса.
Третий пробел этого запуска иного рода: не ложноотрицательный результат внутри детектора, а ветвь без пути к вердикту. detectRTT содержит реальное условие, возвращающее BotKind.HeadlessChrome при rtt.value === 0, и закрепленный реестр никогда его не вызывает. Источник rtt все равно собирается, поэтому значение измеряется и попадает в словарь компонентов, но ни один зарегистрированный детектор его не читает. Чтение одного rtt.ts преувеличивает библиотеку; чтение одного detectors/index.ts скрывает ветвь. Только оба файла вместе показывают собранный сигнал, который не может достичь вердикта.
Какие ветви сводят вердикт к одному движку или значению
Остальные случаи делают видимой специфичность каждой ветви, а специфичность нужна читателю, чтобы понять, какой вердикт вообще применим. Каждая строка ниже это случай, который выполнила проба:
| Детектор | Условие, поданное запуском | Результат |
|---|---|---|
detectUserAgent | строка user agent с HeadlessChrome | headless_chrome |
detectUserAgent | обычная строка настольного Chrome в Windows | false |
detectWebGL | vendor Brian Paul, renderer Mesa OffScreen | headless_chrome |
detectWebGL | renderer ANGLE на адаптере NVIDIA | false |
detectProcess | объект процесса с типом renderer и версией electron | electron |
detectNotificationPermissions | значение разрешения истинно, тип браузера chrome | headless_chrome |
detectEvalLengthInconsistency | evalLength 33 на движке chromium | false |
detectEvalLengthInconsistency | evalLength 37 на движке chromium | true |
detectLanguagesLengthInconsistency | пустой массив языков | headless_chrome |
detectPluginsLengthInconsistency | ноль плагинов, chromium chrome | headless_chrome |
detectWindowExternal | внешняя строка с Sequentum | sequentum |
detectMimeTypesConsistent | значение согласованности ложно | unknown |
detectDocumentAttributes | список ключей document с selenium | selenium |
detectProductSub | значение productSub 20100101 в chrome | unknown |
Две строки заслуживают отдельного внимания, потому что они противоречат интуиции, если читать только имена детекторов. Пара detectEvalLengthInconsistency показывает, что одна и та же функция ничего не возвращает для одной длины и бота для другой на одном и том же движке: 33 принимается, потому что Chromium входит в разрешенный список для этой длины, тогда как 37 отклоняется, потому что Chromium не входит в разрешенный список для той. Функция спрашивает не о том, выглядит ли значение странно, а о том, странно ли оно для обнаруженного движка.
Строка detectNotificationPermissions показывает проверку браузера, которую легко упустить. src/detectors/notification_permissions.ts ничего не возвращает, если только browserKind.value === BrowserKind.Chrome, поэтому одно и то же значение разрешения может дать вердикт только в одном семействе браузеров. Кто проверяет эту поверхность в профиле с другой маркой, увидит отсутствие вердикта и может ошибочно заключить, что значение было принято.
Строки true без имени ведут себя иначе, чем строки с именем, потому что src/api.ts переводит true в BotKind.Unknown. detectEvalLengthInconsistency при длине 37 печатает bot: true в списке случаев, что в итоговом результате становится unknown, тогда как detectMimeTypesConsistent с несогласованным списком уже печатает bot: unknown, потому что возвращает имя напрямую. Отчет, показывающий только unknown, несет меньше информации, чем породивший его детектор, и имя детектора это та часть, которая переживает потерю.
Как сделать запуск воспроизводимым по записи
Проба полезна только если другой человек может повторить ее и получить те же строки. Четыре поля записи это обеспечивают, и каждое закрывает пробел, который оставляют другие.
Первое это commit 193c471ac70530afcebe5b09664c09f611d19218, потому что и detectors/index.ts, и sources/index.ts правятся между версиями, и ключ реестра, существующий сегодня, может исчезнуть завтра. Второе это шаг сборки, потому что вывод esbuild не побайтно одинаков между версиями, поэтому запись отпечатка вывода позволяет отделить изменение кода от изменения инструмента. Третье это соглашение о состояниях, потому что проба пишет state: 0 для успеха и state: -1 для нечитаемого источника, и тот, кто предположит другое соответствие, прочитает каждый результат наоборот. Четвертое это сам список случаев, потому что false от detectWebDriver означает «защита увидела состояние без успеха» в случае 8 и «флаг был ложным» в случае 3, и только метка случая различает их.
Две привычки сохраняют запись честной. Предпочитайте различающую пару одиночному случаю: случаи 3 и 2 вместе показывают, что очистка флага меняет, какой детектор говорит, чего одиночный случай 3 не показывает, а случаи 1 и 8 вместе показывают, что присутствие-и-истина и отсутствие расходятся на одной ветви и сливаются на другой. И никогда не описывайте пробу как тест браузера. Она выполняет закрепленные чистые функции на словарях, написанных вручную; она не читает ни одной страницы, не открывает профиль и не обращается к сервису, поэтому утверждение о том, что сообщает реальная сессия, все еще требует отдельного разрешенного запуска.
Та же дисциплина применима при обновлении исходника. Повторите шаг сборки на новом commit, перечитайте оба файла реестра и сравнивайте список случаев, а не строку сводки. Изменение реестра меняет, какие детекторы вообще могут говорить, изменение внутри детектора меняет, что возвращает один случай, а изменение в источнике меняет, какие состояния вообще достижимы. Сравнение только итоговой строки bot: true или bot: false сливает все три вида в один, и следующему читателю приходится заново выяснять, какой из них произошел.
Как читать вердикт без преувеличения
Вердикт этой библиотеки подтверждает узкое утверждение: в этом commit один из зарегистрированных детекторов, получив переданные ему состояния компонентов, вернул названный тип. Он не подтверждает утверждение о человеке, о намерении или о том, как размещенный сервис обойдется с сессией. Поскольку свертка это перезапись, названный тип может указывать на менее специфичный сигнал, чем другой, который тоже сработал, а поскольку несколько детекторов возвращают просто true, тип unknown может подменить детектор, у которого была конкретная причина.
Храните в записи разбора три вещи, и запись останется полезной. Неизменяемый commit, потому что и порядок реестра, и списки свойств меняются между версиями. Состояния компонентов, потому что каждый детектор проверяет состояние до чтения значения, и ветви ложноотрицательного результата живут в этой проверке. Имя детектора, потому что один запуск может дать разные типы, и только имя говорит, какой из них дал вердикт. Четвертое поле помогает, когда вопрос об одной конкретной поверхности: число собранных источников, чтобы читатель видел, отсутствующий сигнал вообще не измерялся или измерялся и был проигнорирован.
Страница документации о согласованности автоматизации описывает, как BotBrowser держит navigator.webdriver согласованным с загруженным профилем и как проверить эту настройку на контролируемой странице. Этот разбор добавляет сторону чтения: что публичный детектор делает с этим значением, какие сопутствующие поверхности он читает одновременно и какие его собственные ветви молчат. Обе половины отвечают на разные вопросы, и ни одна не заменяет другую.
Для любого вывода о реальном сервисе используйте разрешенный интеграционный результат этого сервиса вместо экстраполяции из этого разбора кода. Состояние компонента, записанное в fixture, и производственный вердикт это разные виды свидетельства, и именно в разрыве между ними происходит преувеличение.
Practical conclusion
Закрепите commit, запустите функции детекторов на явных состояниях компонентов и запишите имя детектора рядом с вердиктом. В этом commit реестр предоставляет 18 детекторов на 23 источника, detectRTT экспортируется, но не регистрируется, очистка флага WebDriver оставляет списки свойств нетронутыми, а две проверки, нечитаемый компонент и документ без фокуса, дают отсутствие вердикта.
Sources
- Реестр детекторов BotD в закрепленном commit
- Реестр источников BotD в закрепленном commit
- Свертка вердикта в
api.ts - Детектор
webdriver.ts - Детектор
window_size.ts - Детектор
rtt.ts - Источник
distinctive_properties.ts - Документация BotBrowser о согласованности автоматизации
Next reading
Используйте сбор данных и происхождение в FingerprintJS OSS для стороны сбора того же поставщика и обнаружение и предотвращение автоматизации, когда вопрос в том, какие поверхности страница может прочитать.
Эта статья входит в руководство: Что такое отпечаток браузера?
Похожие статьи
FingerprintJS OSS: сбор данных и происхождение хеша
ОтпечаткиКак читать сигналы отпечатка браузера
ОтпечаткиЧто такое отпечаток браузера?
ОтпечаткиНаблюдаемость криптографической случайности и отпечатки браузера
Отпечаткиperformance.now: разрешение таймера и отпечаток браузера
ОтпечаткиV8Log Forensics: доказательства выполнения для проверки приватности
Переведите BotBrowser из исследований в продакшн
Используйте эти руководства, чтобы понять модель, а затем перейти к кроссплатформенной валидации, изолированным контекстам и масштабируемому браузерному развертыванию.