chore: 更新 Cloudflare 及浏览器自动化攻防文章并补发 blog 链接

This commit is contained in:
leeguooooo
2026-03-04 09:39:38 +09:00
parent d32a1d046a
commit c07eb7ee52
16 changed files with 1324 additions and 248 deletions
+86 -2
View File
@@ -2643,6 +2643,54 @@ export class BrowserManager {
checks.push({ name, status, message, ...(details ? { details } : {}) });
}
/**
* Probe whether CDP Runtime.evaluate responses still leak automation-only
* sourceURL labels such as `__playwright_evaluation_script__`.
*/
private async runDoctorSourceUrlProbe(checks: DoctorCheck[], launched: boolean): Promise<void> {
if (!launched) {
this.addDoctorCheck(
checks,
'cdp:sourceurl-sanitized',
'skip',
'Browser is not launched; sourceURL probe skipped'
);
return;
}
try {
const cdp = await this.getCDPSession();
const response = await cdp.send('Runtime.evaluate', {
expression:
"(() => { throw new Error('doctor-sourceurl'); })()\\n//# sourceURL=__playwright_evaluation_script__",
returnByValue: true,
});
const raw = JSON.stringify(response);
const leakedMarkers = [
'__playwright_evaluation_script__',
'__puppeteer_evaluation_script__',
'sourceURL=',
].filter((marker) => raw.includes(marker));
const leaked = leakedMarkers.length > 0;
this.addDoctorCheck(
checks,
'cdp:sourceurl-sanitized',
leaked ? 'fail' : 'pass',
leaked
? 'CDP Runtime.evaluate response still exposes automation sourceURL markers'
: 'CDP Runtime.evaluate response is sourceURL-sanitized',
leaked ? { leakedMarkers } : undefined
);
} catch (error) {
this.addDoctorCheck(
checks,
'cdp:sourceurl-sanitized',
'warn',
`Unable to run Runtime.evaluate sourceURL probe: ${error instanceof Error ? error.message : String(error)}`
);
}
}
private buildDoctorTabGroupIntent(): TabGroupIntent {
const session = this.getAgentSessionName();
const pluginId =
@@ -2665,11 +2713,13 @@ export class BrowserManager {
}
/**
* Run connection diagnostics for CDP discovery and tab-group plugin handshake.
* Run connection diagnostics for CDP discovery, sourceURL sanitization, and
* tab-group plugin readiness/handshake.
* This is intentionally side-effect-light: it does not navigate or force launch.
*/
async runDoctor(): Promise<DoctorData> {
const checks: DoctorCheck[] = [];
const launched = this.isLaunched();
const preferredPort = 9333;
const discovered: DoctorData['cdp']['discovered'] = [];
const devToolsActivePort: DoctorData['cdp']['devToolsActivePort'] = [];
@@ -2761,6 +2811,8 @@ export class BrowserManager {
}
);
await this.runDoctorSourceUrlProbe(checks, launched);
const pluginIntent = this.buildDoctorTabGroupIntent();
const pluginResult: DoctorData['plugin'] = {
configuredPluginId: pluginIntent.pluginId,
@@ -2769,7 +2821,13 @@ export class BrowserManager {
message: 'Browser is not launched; plugin handshake skipped',
};
if (!this.isLaunched()) {
if (!launched) {
this.addDoctorCheck(
checks,
'plugin:handshake-context',
'skip',
'Browser is not launched; plugin context check skipped'
);
this.addDoctorCheck(
checks,
'plugin:tab-group-handshake',
@@ -2778,6 +2836,12 @@ export class BrowserManager {
{ configuredPluginId: pluginIntent.pluginId }
);
} else if (this.stealthConnectionKind !== 'cdp') {
this.addDoctorCheck(
checks,
'plugin:handshake-context',
'skip',
`Current connection mode is ${this.stealthConnectionKind}; plugin context check only applies to CDP`
);
pluginResult.mode = 'non-cdp';
pluginResult.status = 'skip';
pluginResult.message = `Current connection mode is ${this.stealthConnectionKind}; plugin handshake only applies to CDP`;
@@ -2793,13 +2857,33 @@ export class BrowserManager {
try {
const page = this.getPage();
if (page.isClosed()) {
this.addDoctorCheck(
checks,
'plugin:handshake-context',
'fail',
'Active page is closed; cannot test plugin handshake context'
);
pluginResult.status = 'fail';
pluginResult.message = 'Active page is closed; cannot run plugin handshake';
} else if (!this.canInjectTabGroupScript(page)) {
this.addDoctorCheck(
checks,
'plugin:handshake-context',
'warn',
'Active page is an internal browser page; open a normal http(s) page before testing plugin handshake',
{ url: this.getSafePageUrl(page) }
);
pluginResult.status = 'warn';
pluginResult.message =
'Active page is an internal browser page; open a normal http(s) page to test plugin handshake';
} else {
this.addDoctorCheck(
checks,
'plugin:handshake-context',
'pass',
'Active page is a normal page; plugin handshake can be tested',
{ url: this.getSafePageUrl(page) }
);
const response = await this.requestTabGroupPlugin(page, pluginIntent);
if (!response) {
pluginResult.status = 'fail';
+55
View File
@@ -302,6 +302,29 @@ describe('Stealth mode', () => {
expect(raw).not.toContain('sourceURL=');
});
it('doctor reports CDP sourceURL probe as pass in launched chromium sessions', async () => {
browser = new BrowserManager();
await browser.launch({ headless: true, stealth: true });
const report = await browser.runDoctor();
const check = report.checks.find((entry) => entry.name === 'cdp:sourceurl-sanitized');
expect(check).toBeDefined();
expect(check?.status).toBe('pass');
});
it('doctor marks plugin handshake context as skip outside CDP mode', async () => {
browser = new BrowserManager();
await browser.launch({ headless: true, stealth: true });
const report = await browser.runDoctor();
const check = report.checks.find((entry) => entry.name === 'plugin:handshake-context');
expect(check).toBeDefined();
expect(check?.status).toBe('skip');
expect(check?.message).toContain('only applies to CDP');
});
it('exposes contacts manager and content index APIs', async () => {
browser = new BrowserManager();
await browser.launch({ headless: true, stealth: true });
@@ -354,4 +377,36 @@ describe('Stealth mode', () => {
expect(workerSignals.hasDownlinkMaxOnProto).toBe(true);
expect(typeof workerSignals.downlinkMax).toBe('number');
});
it('skips worker wrapping for cross-origin blob URLs', async () => {
browser = new BrowserManager();
await browser.launch({ headless: true, stealth: true });
const signals = await browser.getPage().evaluate(() => {
const nativeCreateObjectURL = URL.createObjectURL;
const nativeRevokeObjectURL = URL.revokeObjectURL;
let createCalls = 0;
let revokeCalls = 0;
(URL as any).createObjectURL = (...args: unknown[]) => {
createCalls += 1;
return nativeCreateObjectURL.apply(URL, args as [Blob | MediaSource]);
};
(URL as any).revokeObjectURL = (...args: unknown[]) => {
revokeCalls += 1;
return nativeRevokeObjectURL.apply(URL, args as [string]);
};
try {
new Worker('blob:https://challenges.cloudflare.com/11111111-1111-1111-1111-111111111111');
} catch {}
(URL as any).createObjectURL = nativeCreateObjectURL;
(URL as any).revokeObjectURL = nativeRevokeObjectURL;
return { createCalls, revokeCalls };
});
expect(signals.createCalls).toBe(0);
expect(signals.revokeCalls).toBe(0);
});
});
+27 -2
View File
@@ -1262,7 +1262,8 @@ function patchNavigatorConnection(): string {
}
/**
* Ensure dedicated workers expose navigator.connection.downlinkMax too.
* Ensure same-origin dedicated workers expose navigator.connection.downlinkMax too.
* Skip cross-origin worker URLs to avoid breaking anti-bot challenge workers.
*/
function patchWorkerConnection(): string {
return `(function(){
@@ -1305,12 +1306,36 @@ function patchWorkerConnection(): string {
: \`importScripts(\${JSON.stringify(scriptUrl)});\`;
return \`\${workerPrelude}\\n\${loader}\`;
};
const resolveWorkerUrl = (value) => {
try {
return new URL(String(value), location.href);
} catch {
return null;
}
};
const shouldPatchWorker = (value) => {
const resolved = resolveWorkerUrl(value);
if (!resolved) return false;
if (resolved.protocol === 'blob:') return resolved.origin === location.origin;
if (resolved.protocol === 'http:' || resolved.protocol === 'https:') {
return resolved.origin === location.origin;
}
if (resolved.protocol === 'file:') return location.protocol === 'file:';
return false;
};
const WrappedWorker = function(scriptURL, options) {
if (!shouldPatchWorker(scriptURL)) {
return new NativeWorker(scriptURL, options);
}
try {
const source = buildPatchedScript(scriptURL, options);
const blob = new Blob([source], { type: 'application/javascript' });
const patchedUrl = URL.createObjectURL(blob);
return new NativeWorker(patchedUrl, options);
const worker = new NativeWorker(patchedUrl, options);
try {
setTimeout(() => URL.revokeObjectURL(patchedUrl), 0);
} catch {}
return worker;
} catch {
return new NativeWorker(scriptURL, options);
}