Scope
vision.analyze accepts an HTTP(S) URL as the url input and fetches the image directly at runtime. No SSRF preflight is performed before that fetch — unlike every other web tool in the codebase.
Current state
packages/tools/src/vision.ts (verified: no validateFetchUrl or safeFetchPreflight import). The image URL supplied by the caller (resolvedUrl) is passed verbatim inside the JSON body to the OpenAI completions endpoint. For HTTP(S) URLs, the function takes the raw imageSource and passes it through resolveImageUrl (lines 17–49), which returns it unchanged for any https?:// URL (line 19).
Contrast with web.fetch (packages/tools/src/index.ts:630), web.extractText (index.ts:1172), and web.crawl (index.ts:1352), all of which call safeFetchPreflight before fetching.
The fallback no-key path (lines 182–204) issues a direct fetch(resolvedUrl, { method: 'HEAD' }) with zero SSRF validation, exposing the host to metadata-endpoint probing even without an API key configured.
Issue #188 ([MEDIUM] Restrict LLM vision URL fetch to scheme allowlist (SSRF)) is open but scoped narrowly to "scheme allowlist". The actual gap is the missing safeFetchPreflight call for direct fetches in the fallback path.
Proposed
- Add
safeFetchPreflight(imageSource) before resolveImageUrl when imageSource is an HTTP(S) URL. Reuse the existing helper from packages/core/src/security.ts (exported via resolveAndValidateUrl).
- Remove the fallback HEAD fetch entirely or gate it behind the same preflight.
- Apply
redirect: 'manual' on the fallback HEAD fetch to close the redirect-chain bypass.
Acceptance
vision.analyze with url: "http://169.254.169.254/latest/meta-data/" returns ok: false, output: "URL blocked: ..." without making any network request.
- Existing unit tests for vision pass.
References
Scope
vision.analyzeaccepts an HTTP(S) URL as theurlinput and fetches the image directly at runtime. No SSRF preflight is performed before that fetch — unlike every other web tool in the codebase.Current state
packages/tools/src/vision.ts(verified: novalidateFetchUrlorsafeFetchPreflightimport). The image URL supplied by the caller (resolvedUrl) is passed verbatim inside the JSON body to the OpenAI completions endpoint. For HTTP(S) URLs, the function takes the rawimageSourceand passes it throughresolveImageUrl(lines 17–49), which returns it unchanged for anyhttps?://URL (line 19).Contrast with
web.fetch(packages/tools/src/index.ts:630),web.extractText(index.ts:1172), andweb.crawl(index.ts:1352), all of which callsafeFetchPreflightbefore fetching.The fallback no-key path (lines 182–204) issues a direct
fetch(resolvedUrl, { method: 'HEAD' })with zero SSRF validation, exposing the host to metadata-endpoint probing even without an API key configured.Issue #188 (
[MEDIUM] Restrict LLM vision URL fetch to scheme allowlist (SSRF)) is open but scoped narrowly to "scheme allowlist". The actual gap is the missingsafeFetchPreflightcall for direct fetches in the fallback path.Proposed
safeFetchPreflight(imageSource)beforeresolveImageUrlwhenimageSourceis an HTTP(S) URL. Reuse the existing helper frompackages/core/src/security.ts(exported viaresolveAndValidateUrl).redirect: 'manual'on the fallback HEAD fetch to close the redirect-chain bypass.Acceptance
vision.analyzewithurl: "http://169.254.169.254/latest/meta-data/"returnsok: false, output: "URL blocked: ..."without making any network request.References
packages/tools/src/vision.tslines 17–49, 182–204