From 8635f0f5eb961b09deb4b93c7a9db70eceaeaaec Mon Sep 17 00:00:00 2001 From: paisley <8197966+su8su@users.noreply.github.com> Date: Mon, 25 May 2026 18:28:10 +0800 Subject: [PATCH] fix: default web_fetch SSRF policy for fake-IP proxy DNS (#1065) --- electron/utils/openclaw-auth.ts | 58 ++++++++++++++++++++++++++++++-- tests/unit/openclaw-auth.test.ts | 47 ++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 2 deletions(-) diff --git a/electron/utils/openclaw-auth.ts b/electron/utils/openclaw-auth.ts index f5f99e6..d0f85ee 100644 --- a/electron/utils/openclaw-auth.ts +++ b/electron/utils/openclaw-auth.ts @@ -1894,6 +1894,53 @@ export async function syncGatewayTokenToConfig(token: string): Promise { }); } +/** + * Default web_fetch SSRF policy for fake-IP / transparent-proxy environments + * (e.g. Clash/Surge resolving public hostnames into 198.18.0.0/15). OpenClaw's + * web_fetch tool does not read browser.ssrfPolicy — it uses tools.web.fetch only. + */ +function ensureWebFetchSsrfPolicyInConfig(config: Record): boolean { + const tools = ( + config.tools && typeof config.tools === 'object' + ? { ...(config.tools as Record) } + : {} + ) as Record; + const web = ( + tools.web && typeof tools.web === 'object' + ? { ...(tools.web as Record) } + : {} + ) as Record; + const fetch = ( + web.fetch && typeof web.fetch === 'object' + ? { ...(web.fetch as Record) } + : {} + ) as Record; + + const ssrfPolicy = ( + fetch.ssrfPolicy && typeof fetch.ssrfPolicy === 'object' + ? { ...(fetch.ssrfPolicy as Record) } + : {} + ) as Record; + + let changed = false; + if (ssrfPolicy.allowRfc2544BenchmarkRange === undefined) { + ssrfPolicy.allowRfc2544BenchmarkRange = true; + changed = true; + } + if (ssrfPolicy.allowIpv6UniqueLocalRange === undefined) { + ssrfPolicy.allowIpv6UniqueLocalRange = true; + changed = true; + } + + if (!changed) return false; + + fetch.ssrfPolicy = ssrfPolicy; + web.fetch = fetch; + tools.web = web; + config.tools = tools; + return true; +} + /** * Ensure browser automation is enabled in ~/.openclaw/openclaw.json. */ @@ -1931,11 +1978,13 @@ export async function syncBrowserConfigToOpenClaw(): Promise { changed = true; } + changed = ensureWebFetchSsrfPolicyInConfig(config) || changed; + if (!changed) return; config.browser = browser; await writeOpenClawJson(config); - console.log('Synced browser config to openclaw.json'); + console.log('Synced browser and web_fetch config to openclaw.json'); }); } @@ -2051,6 +2100,11 @@ export async function batchSyncConfigFields(token: string): Promise { modified = true; } + // ── web_fetch SSRF policy (fake-IP / transparent-proxy environments) ── + if (ensureWebFetchSsrfPolicyInConfig(config)) { + modified = true; + } + // ── Session idle minutes ── const session = ( config.session && typeof config.session === 'object' @@ -2069,7 +2123,7 @@ export async function batchSyncConfigFields(token: string): Promise { if (modified) { await writeOpenClawJson(config); - console.log('Synced gateway token, browser config, and session idle to openclaw.json'); + console.log('Synced gateway token, browser config, web_fetch SSRF policy, and session idle to openclaw.json'); } }); } diff --git a/tests/unit/openclaw-auth.test.ts b/tests/unit/openclaw-auth.test.ts index 622a46c..76fbebf 100644 --- a/tests/unit/openclaw-auth.test.ts +++ b/tests/unit/openclaw-auth.test.ts @@ -1820,3 +1820,50 @@ describe('ensureOpenClawProviderAgentRuntimePins', () => { expect(after).toEqual(before); }); }); + +describe('batchSyncConfigFields', () => { + beforeEach(async () => { + vi.resetModules(); + vi.restoreAllMocks(); + await rm(testHome, { recursive: true, force: true }); + await rm(testUserData, { recursive: true, force: true }); + }); + + it('seeds web_fetch SSRF policy for fake-IP proxy environments', async () => { + await writeOpenClawJson({ gateway: { auth: { mode: 'token', token: 'old' } } }); + + const { batchSyncConfigFields } = await import('@electron/utils/openclaw-auth'); + await batchSyncConfigFields('new-token'); + + const config = await readOpenClawJson(); + const fetch = (config.tools as Record).web as Record; + const ssrfPolicy = (fetch.fetch as Record).ssrfPolicy as Record; + expect(ssrfPolicy.allowRfc2544BenchmarkRange).toBe(true); + expect(ssrfPolicy.allowIpv6UniqueLocalRange).toBe(true); + }); + + it('does not override explicit web_fetch SSRF policy opt-outs', async () => { + await writeOpenClawJson({ + gateway: { auth: { mode: 'token', token: 'old' } }, + tools: { + web: { + fetch: { + ssrfPolicy: { + allowRfc2544BenchmarkRange: false, + allowIpv6UniqueLocalRange: false, + }, + }, + }, + }, + }); + + const { batchSyncConfigFields } = await import('@electron/utils/openclaw-auth'); + await batchSyncConfigFields('new-token'); + + const config = await readOpenClawJson(); + const fetch = (config.tools as Record).web as Record; + const ssrfPolicy = (fetch.fetch as Record).ssrfPolicy as Record; + expect(ssrfPolicy.allowRfc2544BenchmarkRange).toBe(false); + expect(ssrfPolicy.allowIpv6UniqueLocalRange).toBe(false); + }); +});