diff --git a/src/agents/tools/web-fetch.ts b/src/agents/tools/web-fetch.ts index 92f94bf3a28..c9823726f2e 100644 --- a/src/agents/tools/web-fetch.ts +++ b/src/agents/tools/web-fetch.ts @@ -458,6 +458,9 @@ type WebFetchRuntimeParams = FirecrawlRuntimeParams & { cacheTtlMs: number; userAgent: string; readabilityEnabled: boolean; + ssrfPolicy?: { + allowRfc2544BenchmarkRange?: boolean; + }; }; function toFirecrawlContentParams( @@ -512,8 +515,10 @@ async function maybeFetchFirecrawlWebFetchPayload( } async function runWebFetch(params: WebFetchRuntimeParams): Promise> { + // Include ssrfPolicy in cache key to prevent cross-policy cache bypass + const ssrfPolicySuffix = params.ssrfPolicy?.allowRfc2544BenchmarkRange ? ":rfc2544" : ""; const cacheKey = normalizeCacheKey( - `fetch:${params.url}:${params.extractMode}:${params.maxChars}`, + `fetch:${params.url}:${params.extractMode}:${params.maxChars}${ssrfPolicySuffix}`, ); const cached = readCache(FETCH_CACHE, cacheKey); if (cached) { @@ -534,11 +539,18 @@ async function runWebFetch(params: WebFetchRuntimeParams): Promise Promise) | null = null; let finalUrl = params.url; + + // Build SSRF policy from config + const policy = params.ssrfPolicy?.allowRfc2544BenchmarkRange + ? { allowRfc2544BenchmarkRange: true } + : undefined; + try { const result = await fetchWithWebToolsNetworkGuard({ url: params.url, maxRedirects: params.maxRedirects, timeoutSeconds: params.timeoutSeconds, + policy, init: { headers: { Accept: "text/markdown, text/html;q=0.9, */*;q=0.1", @@ -741,6 +753,7 @@ export function createWebFetchTool(options?: { return null; } const readabilityEnabled = resolveFetchReadabilityEnabled(fetch); + const ssrfPolicy = fetch?.ssrfPolicy; const firecrawl = resolveFirecrawlConfig(fetch); const runtimeFirecrawlActive = options?.runtimeFirecrawl?.active; const shouldResolveFirecrawlApiKey = @@ -787,6 +800,7 @@ export function createWebFetchTool(options?: { cacheTtlMs: resolveCacheTtlMs(fetch?.cacheTtlMinutes, DEFAULT_CACHE_TTL_MINUTES), userAgent, readabilityEnabled, + ssrfPolicy, firecrawlEnabled, firecrawlApiKey, firecrawlBaseUrl, diff --git a/src/config/types.tools.ts b/src/config/types.tools.ts index f42fa365f6f..adad35ce083 100644 --- a/src/config/types.tools.ts +++ b/src/config/types.tools.ts @@ -505,6 +505,11 @@ export type ToolsConfig = { userAgent?: string; /** Use Readability to extract main content (default: true). */ readability?: boolean; + /** SSRF policy configuration for web_fetch. */ + ssrfPolicy?: { + /** Allow RFC 2544 benchmark range IPs (198.18.0.0/15) for fake-IP proxy compatibility (e.g., Clash TUN mode, Surge). */ + allowRfc2544BenchmarkRange?: boolean; + }; firecrawl?: { /** Enable Firecrawl fallback (default: true when apiKey is set). */ enabled?: boolean; diff --git a/src/config/zod-schema.agent-runtime.ts b/src/config/zod-schema.agent-runtime.ts index 10f0f8637e9..775d619b92f 100644 --- a/src/config/zod-schema.agent-runtime.ts +++ b/src/config/zod-schema.agent-runtime.ts @@ -332,6 +332,12 @@ export const ToolsWebFetchSchema = z maxRedirects: z.number().int().nonnegative().optional(), userAgent: z.string().optional(), readability: z.boolean().optional(), + ssrfPolicy: z + .object({ + allowRfc2544BenchmarkRange: z.boolean().optional(), + }) + .strict() + .optional(), firecrawl: z .object({ enabled: z.boolean().optional(),