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
+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`).