feat(stealth): 优化隐身对抗并引入双版本发布体系

- 将 CreepJS like headless 指标优化到 0%(headless/stealth 维持 0%)

- 新增 ActiveText 与 prefers-color-scheme 探针修复

- 版本号采用 <upstream>-fork.<fork> 格式并在 --version 输出 upstream/fork

- 更新 README、SKILL 与 docs 中的版本体系说明
This commit is contained in:
leeguooooo
2026-02-24 15:29:11 +09:00
parent 4c6afe3e69
commit ea2e93dbba
13 changed files with 285 additions and 62 deletions
+2 -2
View File
@@ -1542,7 +1542,7 @@ export class BrowserManager {
// Expand ~ to home directory since it won't be shell-expanded
const profilePath = options.profile!.replace(/^~\//, os.homedir() + '/');
context = await launcher.launchPersistentContext(profilePath, {
headless: options.headless ?? true,
headless: options.headless ?? false,
executablePath: options.executablePath,
args: baseArgs,
viewport,
@@ -1558,7 +1558,7 @@ export class BrowserManager {
} else {
// Regular ephemeral browser
this.browser = await launcher.launch({
headless: options.headless ?? true,
headless: options.headless ?? false,
executablePath: options.executablePath,
args: baseArgs,
});
+40
View File
@@ -95,6 +95,46 @@ describe('Stealth mode', () => {
expect(userAgentSignals.workerUA).not.toContain('HeadlessChrome');
});
it('neutralizes the css webdriver heuristic probe', async () => {
browser = new BrowserManager();
await browser.launch({ headless: true, stealth: true });
const signals = await browser.getPage().evaluate(() => ({
probe: CSS.supports('border-end-end-radius: initial'),
baseline: CSS.supports('display: block'),
webdriver: navigator.webdriver,
inNavigator: 'webdriver' in navigator,
}));
expect(signals.probe).toBe(false);
expect(signals.baseline).toBe(true);
expect(signals.webdriver).toBeUndefined();
expect(signals.inNavigator).toBe(false);
});
it('neutralizes creepjs prefers-color-scheme light probe', async () => {
browser = new BrowserManager();
await browser.launch({ headless: true, stealth: true });
const signals = await browser.getPage().evaluate(() => {
const node = document.createElement('div');
node.setAttribute('style', 'background-color: ActiveText');
document.body.appendChild(node);
const activeTextColor = getComputedStyle(node).backgroundColor;
node.remove();
return {
activeTextColor,
prefersLight: matchMedia('(prefers-color-scheme: light)').matches,
prefersDark: matchMedia('(prefers-color-scheme: dark)').matches,
};
});
expect(signals.activeTextColor).not.toBe('rgb(255, 0, 0)');
expect(signals.prefersLight).toBe(false);
expect(typeof signals.prefersDark).toBe('boolean');
});
it('exposes realistic mimeTypes/pdf/share signals', async () => {
browser = new BrowserManager();
await browser.launch({ headless: true, stealth: true });
+126
View File
@@ -190,6 +190,7 @@ function buildStealthScript(options: StealthScriptOptions): string {
return [
configScript,
patchNavigatorWebdriver(),
patchCssSupportsWebdriverHeuristic(),
patchChromeRuntime(),
patchNavigatorLanguages(),
patchNavigatorPluginsAndMimeTypes(),
@@ -201,11 +202,13 @@ function buildStealthScript(options: StealthScriptOptions): string {
patchScreenAvailability(),
patchNavigatorHardwareConcurrency(),
patchNotificationPermission(),
patchActiveTextColorHeuristic(),
patchNavigatorConnection(),
patchWorkerConnection(),
patchNavigatorShare(),
patchNavigatorContacts(),
patchContentIndex(),
patchPrefersColorSchemeHeuristic(),
patchPdfViewerEnabled(),
patchMediaDevices(),
patchUserAgentData(),
@@ -219,6 +222,44 @@ function buildStealthScript(options: StealthScriptOptions): string {
// Individual patches
// ---------------------------------------------------------------------------
/**
* CreepJS uses CSS.supports('border-end-end-radius: initial') + webdriver
* undefined to infer automation. Keep this one probe neutral.
*/
function patchCssSupportsWebdriverHeuristic(): string {
return `(function(){
if (typeof CSS === 'undefined' || typeof CSS.supports !== 'function') return;
const nativeSupports = CSS.supports.bind(CSS);
const normalize = (value) => String(value).replace(/\\s+/g, ' ').trim().toLowerCase();
const target = 'border-end-end-radius: initial';
const patchedSupports = function(...args) {
if (args.length === 1 && normalize(args[0]) === target) {
return false;
}
if (args.length >= 2 && normalize(args[0] + ': ' + args[1]) === target) {
return false;
}
return nativeSupports(...args);
};
try {
Object.defineProperty(patchedSupports, 'name', { value: 'supports', configurable: true });
Object.defineProperty(patchedSupports, 'toString', {
value: () => nativeSupports.toString(),
configurable: true,
});
} catch {}
try {
Object.defineProperty(CSS, 'supports', {
value: patchedSupports,
configurable: true,
writable: true,
});
} catch {
try { CSS.supports = patchedSupports; } catch {}
}
})();`;
}
/**
* Remove navigator.webdriver entirely.
* Modern detection checks both value and property presence (`'webdriver' in navigator`).
@@ -642,6 +683,46 @@ function patchNotificationPermission(): string {
})();`;
}
/**
* CreepJS probes `background-color: ActiveText` and flags Chromium when the
* computed value resolves to `rgb(255, 0, 0)`. Rewrite only that exact probe.
*/
function patchActiveTextColorHeuristic(): string {
return `(function(){
if (typeof Element === 'undefined' || !Element.prototype) return;
const nativeSetAttribute = Element.prototype.setAttribute;
if (typeof nativeSetAttribute !== 'function') return;
const normalize = (value) => String(value).replace(/\\s+/g, ' ').trim().toLowerCase();
const probeStyle = 'background-color: activetext';
const replacement = 'background-color: rgb(0, 0, 0)';
const patchedSetAttribute = function(name, value) {
if (String(name).toLowerCase() === 'style' && normalize(value) === probeStyle) {
return nativeSetAttribute.call(this, name, replacement);
}
return nativeSetAttribute.call(this, name, value);
};
try {
Object.defineProperty(patchedSetAttribute, 'name', {
value: 'setAttribute',
configurable: true,
});
Object.defineProperty(patchedSetAttribute, 'toString', {
value: () => nativeSetAttribute.toString(),
configurable: true,
});
} catch {}
try {
Object.defineProperty(Element.prototype, 'setAttribute', {
value: patchedSetAttribute,
configurable: true,
writable: true,
});
} catch {
try { Element.prototype.setAttribute = patchedSetAttribute; } catch {}
}
})();`;
}
/**
* Add missing connection.downlinkMax in Chromium headless environments.
*/
@@ -750,6 +831,51 @@ function patchWorkerConnection(): string {
})();`;
}
/**
* CreepJS marks light-scheme defaults as a weak headless signal. Keep
* `(prefers-color-scheme: light)` neutral without affecting other media queries.
*/
function patchPrefersColorSchemeHeuristic(): string {
return `(function(){
if (typeof window.matchMedia !== 'function') return;
const nativeMatchMedia = window.matchMedia.bind(window);
const normalize = (query) => String(query).replace(/\\s+/g, ' ').trim().toLowerCase();
const prefersLight = '(prefers-color-scheme: light)';
const patchMediaQueryList = (mql) => {
if (!mql || typeof mql !== 'object') return mql;
return new Proxy(mql, {
get(target, prop, receiver) {
if (prop === 'matches') return false;
return Reflect.get(target, prop, receiver);
},
});
};
const patchedMatchMedia = function(query) {
const mql = nativeMatchMedia(query);
if (normalize(query) === prefersLight) {
return patchMediaQueryList(mql);
}
return mql;
};
try {
Object.defineProperty(patchedMatchMedia, 'name', { value: 'matchMedia', configurable: true });
Object.defineProperty(patchedMatchMedia, 'toString', {
value: () => nativeMatchMedia.toString(),
configurable: true,
});
} catch {}
try {
Object.defineProperty(window, 'matchMedia', {
value: patchedMatchMedia,
configurable: true,
writable: true,
});
} catch {
try { window.matchMedia = patchedMatchMedia; } catch {}
}
})();`;
}
/**
* Add share/canShare APIs expected on modern Chromium desktop.
*/