fix(stealth): neutralize disable-devtool auto bootstrap

This commit is contained in:
leeguooooo
2026-03-05 11:11:36 +09:00
parent 19aefb7eb8
commit a71198d591
7 changed files with 158 additions and 7 deletions
+10
View File
@@ -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
+1 -1
View File
@@ -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"
@@ -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. 时序分布塑形(降低机械统计特征)
@@ -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 等摩擦控制)
+1 -1
View File
@@ -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",
+28
View File
@@ -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 });
+60
View File
@@ -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`).