From a71198d591d946e6d18c22c9e07992e96d9165f4 Mon Sep 17 00:00:00 2001 From: leeguooooo Date: Thu, 5 Mar 2026 11:11:36 +0900 Subject: [PATCH] fix(stealth): neutralize disable-devtool auto bootstrap --- CHANGELOG.md | 10 ++++ cli/Cargo.toml | 2 +- ...r-stealth-attack-defense-playbook.zh-CN.md | 35 ++++++++++- ...re-turnstile-stability-principles.zh-CN.md | 28 +++++++-- package.json | 2 +- src/stealth.test.ts | 28 +++++++++ src/stealth.ts | 60 +++++++++++++++++++ 7 files changed, 158 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d4c000..47ed4ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # agent-browser +## 0.16.1-fork.5 + +### Patch Changes + +- Harden runtime stealth against anti-debug self-destruct flows on high-risk sites: + - neutralize `disable-devtool` auto bootstrap probes by hiding the `[disable-devtool-auto]` selector entry point + - preserve normal selector behavior for non-target queries to minimize side effects + - add regression tests covering the selector patch boundary +- Expand security design docs with the anti-debug execution-plane model and clarify why page self-close/redirect is a separate surface from fingerprint scoring. + ## 0.15.2-fork.0 ### Patch Changes diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 0524b47..87e7a0b 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "agent-browser-stealth" -version = "0.16.1-fork.4" +version = "0.16.1-fork.5" edition = "2021" description = "Stealth browser automation CLI for AI agents with anti-bot evasions" license = "Apache-2.0" diff --git a/docs/blog/agent-browser-stealth-attack-defense-playbook.zh-CN.md b/docs/blog/agent-browser-stealth-attack-defense-playbook.zh-CN.md index ed73fd7..0401df6 100644 --- a/docs/blog/agent-browser-stealth-attack-defense-playbook.zh-CN.md +++ b/docs/blog/agent-browser-stealth-attack-defense-playbook.zh-CN.md @@ -109,6 +109,27 @@ flowchart TD > 执行链路保护优先于信号修饰。 +### 1.6 反调试执行面:与指纹评分面并行 + +很多站点并不只依赖指纹评分,还会部署“主动处置型反调试”脚本。 + +典型路径: + +```mermaid +flowchart LR + A["页面启动"] --> B["反调试探测"] + B --> C{"命中?"} + C -->|是| D["close/back/redirect"] + C -->|否| E["继续业务逻辑"] +``` + +该路径与指纹评分面的关系: + +1. 指纹评分决定“挑战/放行/降权” +2. 反调试处置决定“页面是否继续可用” + +因此,“页面自关闭”不能直接推断为“指纹被识别”,更常见是反调试链路触发。 + --- ## 二、控制面(分层设计) @@ -157,6 +178,18 @@ flowchart LR - 优先治理高频、可解释的探测路径 - 对跨域挑战链路对象设置严格注入边界 +#### 2.4.1 反调试脚本治理(以 disable-devtool 类库为例) + +反调试脚本通常通过固定启动入口触发(例如 `disable-devtool-auto` 标记)。 + +可行控制策略: + +1. 仅抑制其自动启动入口,避免触发主动处置 +2. 不改写通用查询/脚本加载语义,避免影响业务页面 +3. 将治理范围限制在高置信度触发点,控制副作用面 + +这类策略的本质是“执行面隔离”,不是“伪造更多指纹”。 + ### 2.5 行为与会话控制 目标:塑形时间分布,减少上下文漂移。 @@ -183,7 +216,7 @@ Turnstile 场景下的关键控制面可抽象为: 控制面设计通常按以下优先级推进: -1. 执行完整性(保证链路可运行) +1. 执行完整性(保证链路可运行,包含反调试触发面治理) 2. 一致性约束集合(消除跨表面矛盾) 3. 稀有性控制(避免低频组合叠加) 4. 时序分布塑形(降低机械统计特征) diff --git a/docs/blog/cloudflare-turnstile-stability-principles.zh-CN.md b/docs/blog/cloudflare-turnstile-stability-principles.zh-CN.md index 7318f3c..ff6edfe 100644 --- a/docs/blog/cloudflare-turnstile-stability-principles.zh-CN.md +++ b/docs/blog/cloudflare-turnstile-stability-principles.zh-CN.md @@ -76,7 +76,21 @@ flowchart TD 风险评分的关键不是“拟合某种固定画像”,而是“同一身份在多表面是否自洽”。 -### 1.5 作用域绑定:hostname / action / cdata +### 1.5 反调试脚本是独立攻击面,不等同于指纹命中 + +在高风控站点中,常见一类“主动防御脚本”(例如 `disable-devtool`): + +1. 页面启动后检测开发者工具、控制台钩子、调试停顿特征 +2. 命中后执行主动处置(`window.close` / `history.back` / 跳转错误页) + +这条链路的关键点: + +- 它是“反调试执行面”,不是“指纹评分面”的简单子集 +- 会表现为“页面自关闭/自跳转”,但根因可能是运行时反调试触发 + +工程上必须将其与“指纹问题”分层处理,否则容易误判。 + +### 1.6 作用域绑定:hostname / action / cdata 服务端校验时提供用于绑定业务语义的字段: @@ -96,7 +110,7 @@ flowchart LR D --> G["降低跨流程重放收益"] ``` -### 1.6 Token 状态机(能力令牌视角) +### 1.7 Token 状态机(能力令牌视角) 从能力令牌视角,token 生命周期可抽象为: @@ -114,7 +128,7 @@ stateDiagram-v2 设计目标是让“非法路径”快速失败,并且失败类型可被服务端语义区分。 -### 1.7 攻击树(高层) +### 1.8 攻击树(高层) Turnstile 的主要攻击目标可以抽象为: @@ -171,6 +185,12 @@ flowchart LR - “执行完整性”是上游条件 - “信号修饰”是下游优化 +补充:反调试脚本属于执行控制的一部分。 + +- 若页面含有主动防御脚本(如 `disable-devtool-auto` 入口),需要把“自动触发入口”与“正常业务脚本”分开处理 +- 最小化目标是阻断其自动化处置链路,避免误伤挑战脚本/跨域组件 +- 处理边界应是“只抑制高风险触发入口,不重写通用 DOM 语义” + ### 2.3 服务端消费控制:把 token 当作能力消费 服务端消费控制的设计关键在于“放行条件定义”,而不是“接口调用细节”。 @@ -228,7 +248,7 @@ Pre-clearance 通过后可产生 clearance,用于后续 WAF 挑战联动。 Turnstile 攻防设计通常按以下优先级推进: 1. 服务端消费语义闭环(真实性 + 作用域 + 绑定 + 单次性 + 时效性) -2. 挑战执行链路完整性(跨域语义保护) +2. 挑战执行链路完整性(跨域语义保护 + 反调试触发面治理) 3. 信号一致性(减少跨字段矛盾) 4. 行为时序(降低机械分布) 5. 体验优化(clearance 等摩擦控制) diff --git a/package.json b/package.json index 9e4a374..c9cac44 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agent-browser-stealth", - "version": "0.16.1-fork.4", + "version": "0.16.1-fork.5", "description": "Stealth browser automation CLI for AI agents with anti-bot evasions", "type": "module", "main": "dist/daemon.js", diff --git a/src/stealth.test.ts b/src/stealth.test.ts index ced817b..fd1d9f0 100644 --- a/src/stealth.test.ts +++ b/src/stealth.test.ts @@ -126,6 +126,34 @@ describe('Stealth mode', () => { expect(signals.inNavigator).toBe(false); }); + it('hides disable-devtool auto bootstrap selector while keeping normal selectors intact', async () => { + browser = new BrowserManager(); + await browser.launch({ headless: true, stealth: true }); + + const signals = await browser.getPage().evaluate(() => { + const marker = document.createElement('meta'); + marker.setAttribute('data-ab-marker', 'ok'); + document.head.appendChild(marker); + + const dd = document.createElement('script'); + dd.setAttribute('disable-devtool-auto', ''); + document.head.appendChild(dd); + + const hiddenOne = document.querySelector('[disable-devtool-auto]') === null; + const hiddenCount = document.querySelectorAll('[disable-devtool-auto]').length; + const normalSelectorWorks = !!document.querySelector('[data-ab-marker="ok"]'); + + dd.remove(); + marker.remove(); + + return { hiddenOne, hiddenCount, normalSelectorWorks }; + }); + + expect(signals.hiddenOne).toBe(true); + expect(signals.hiddenCount).toBe(0); + expect(signals.normalSelectorWorks).toBe(true); + }); + it('neutralizes creepjs prefers-color-scheme light probe', async () => { browser = new BrowserManager(); await browser.launch({ headless: true, stealth: true }); diff --git a/src/stealth.ts b/src/stealth.ts index 902e4c4..9e717f8 100644 --- a/src/stealth.ts +++ b/src/stealth.ts @@ -292,6 +292,7 @@ function buildStealthScript(options: StealthScriptOptions): string { configScript, patchNavigatorWebdriver(), patchCssSupportsWebdriverHeuristic(), + patchDisableDevtoolAutoBootstrap(), patchChromeRuntime(), patchChromeLegacyApis(), patchIframeContentWindow(), @@ -366,6 +367,65 @@ function patchCssSupportsWebdriverHeuristic(): string { })();`; } +/** + * Some high-risk sites ship disable-devtool with auto bootstrap via + * document.querySelector('[disable-devtool-auto]'). + * + * Hiding only this selector prevents the default self-close/redirect behavior + * without affecting normal selector queries. + */ +function patchDisableDevtoolAutoBootstrap(): string { + return `(function(){ + if (typeof Document === 'undefined') return; + const AUTO_SELECTOR = '[disable-devtool-auto]'; + const NEVER_MATCH_SELECTOR = 'script[__ab_disable_devtool_never_match__="1"]'; + const normalize = (value) => { + if (typeof value !== 'string') return ''; + return value.replace(/\\s+/g, '').toLowerCase(); + }; + const shouldHideSelector = (selector) => normalize(selector) === AUTO_SELECTOR; + + const patchQueryMethod = (proto, method) => { + if (!proto) return; + const native = proto[method]; + if (typeof native !== 'function') return; + + const wrapped = function(selector, ...args) { + if (shouldHideSelector(selector)) { + return native.call(this, NEVER_MATCH_SELECTOR, ...args); + } + return native.call(this, selector, ...args); + }; + + try { + Object.defineProperty(wrapped, 'name', { + value: native.name, + configurable: true, + }); + Object.defineProperty(wrapped, 'toString', { + value: () => native.toString(), + configurable: true, + }); + } catch {} + + try { + Object.defineProperty(proto, method, { + value: wrapped, + configurable: true, + writable: true, + }); + } catch {} + }; + + patchQueryMethod(Document.prototype, 'querySelector'); + patchQueryMethod(Document.prototype, 'querySelectorAll'); + if (typeof Element !== 'undefined') { + patchQueryMethod(Element.prototype, 'querySelector'); + patchQueryMethod(Element.prototype, 'querySelectorAll'); + } +})();`; +} + /** * Remove navigator.webdriver entirely. * Modern detection checks both value and property presence (`'webdriver' in navigator`).