You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Bug]: webSearchBridge.endpoint skips destination policy, so a metadata-address endpoint receives the serving provider API key #4519
The web-search bridge's endpoint option accepts any parseable http or https URL and does not run the destination policy that provider baseUrl values go through. resolveOllamaWebSearchEndpoint and the config validation in src/config.ts check the URL shape only — no providerDestinationConfigError, so no loopback, RFC1918 or cloud-metadata classification.
The consequence is narrow but real: an operator endpoint of http://169.254.169.254/ receives the serving provider's API key, because the ollama backend is the one backend that spends the serving provider's own credential rather than a sidecar credential.
Scope
This is ollama-only and predates #4515. The other backends armed by #4515 ignore webSearchBridge.endpoint entirely — their destinations are executor-owned (the ChatGPT forward pin, the Anthropic sidecar baseUrl which does pass config-time destination policy, the api.x.ai origin, the registry CCA URL, and the hardcoded api.exa.ai). #4515 did not widen this; it was found while reviewing that change and is filed separately so it is not attributed to it.
It also requires operator configuration rather than anything a client can reach. webSearchBridge.enabled must be true, backend must be ollama, and the endpoint must be written into config. That makes it a defense-in-depth gap rather than a remote defect — but the same argument was available for every provider baseUrl, and those are classified anyway.
Suggested fix
Run the same destination classification on webSearchBridge.endpoint that providerBaseUrlConfigError and providerDestinationConfigError run on a provider baseUrl, at config-validation time so a bad value is refused at the write boundary rather than at send time. If a local Ollama endpoint on loopback is a legitimate configuration, that is the case to decide deliberately — either allow it explicitly the way allowPrivateNetwork is an explicit opt-in elsewhere, or require the operator to say so.
Verification
Not fixed here; this is a report. Found during the security review of #4515 at 5b707d3a5c, reading src/web-search/passthrough-bridge.ts around resolveOllamaWebSearchEndpoint and src/config.ts around the webSearchBridge schema.
설명
이 이슈는 운영자가 providers.<name>.webSearchBridge를 켤 때 endpoint가 어디로 가는지 검사하지 않아서, 제공자 baseUrl에는 이미 있는 목적지 정책이 브리지에는 빠진다는 보안 구멍 보고입니다. 현재 dev HEAD(d7c7b493b)를 기준으로 보면 보고 내용이 그대로 맞습니다.
흐름을 초등학생도 따라갈 수 있게 풀면 이렇습니다. 웹 검색 브리지는 원래 Ollama Cloud로 검색을 대신 날리고, 그 자리에 모델이 쓸 검색 결과를 넣어 주는 옵션입니다. 이 기능을 켜려면 설정에 webSearchBridge.enabled: true와 backend: "ollama"가 필요합니다. 이때 검색 요청을 어디로 보낼지는 resolveOllamaWebSearchEndpoint가 정합니다. 운영자가 endpoint를 직접 쓰면 그 URL을 그대로 쓰고, 안 쓰면 baseUrl이 진짜 Ollama Cloud 원본일 때만 기본 경로를 붙입니다.
문제는 그 endpoint를 받을 때입니다. src/config.ts의 providerWebSearchBridgeConfigError는 URL이 http/https로 파싱되는지만 봅니다. 같은 파일에서 제공자 baseUrl은 providerBaseUrlConfigError 다음에 providerDestinationConfigError로 루프백·사설망·클라우드 메타데이터(예: 169.254.169.254)를 막습니다. 그런데 브리지 endpoint에는 그 두 번째 검사가 없습니다. 주석에도 "여기선 모양만 보고, 키를 보내기 전에 다시 검사한다"고 적혀 있지만, 실제로 planPassthroughWebSearchBridge가 다시 하는 일은 originOf로 URL이 깨지지 않았는지만 보는 수준입니다. 목적지 분류(assessDestination)는 호출되지 않습니다.
그다음 runOllamaWebSearch는 그 endpoint로 Authorization: Bearer <제공자 apiKey>를 붙여 POST합니다. 그래서 운영자가 실수로(또는 악의적 설정으로) http://169.254.169.254/ 같은 주소를 넣으면, 그 주소로 제공자 API 키가 나갑니다. 클라이언트가 마음대로 바꿀 수 있는 값은 아니고, 운영자 설정이 필요합니다. 그래도 baseUrl은 같은 이유로 이미 막고 있으니, 브리지만 빠지는 건 방어 깊이 구멍입니다.
범위도 이슈 본문과 같습니다. 지금 dev에서 planPassthroughWebSearchBridge는 여전히 backend !== "ollama"면 바로 꺼집니다. 열려 있는 #4515가 다른 백엔드를 무장시켜도, 그쪽은 이슈가 말한 대로 webSearchBridge.endpoint를 안 쓰고 각자 고정/사이드카/baseUrl 경로를 탑니다. 그래서 이 구멍은 ollama 백엔드 + 명시 endpoint에 국한되고, #4515에 얹어 설명할 문제가 아닙니다. 따로 고치는 게 맞습니다.
고치는 방향도 이미 레포 안에 있습니다. providerDestinationConfigError / assessDestination과 같은 분류를 webSearchBridge.endpoint에 설정 검증 시점에 돌리면, 쓰기 경계에서 거절됩니다. 로컬 Ollama를 루프백으로 쓰고 싶다면 allowPrivateNetwork처럼 명시 옵트인이 필요한지 메인테이너가 정하면 됩니다. 메타데이터 주소는 옵트인 없이 계속 막아야 합니다.
src/config.ts - providerWebSearchBridgeConfigError가 endpoint를 http(s) 파싱만 하고 providerDestinationConfigError/assessDestination을 호출하지 않는다 src/web-search/passthrough-bridge.ts의 resolveOllamaWebSearchEndpoint - 설정된 endpoint를 originOf 통과만 하면 그대로 반환해 메타데이터/사설망을 거르지 않는다 src/web-search/passthrough-bridge.ts의 planPassthroughWebSearchBridge - 주석의 "키 보내기 전 재검증"이 목적지 정책이 아니라 URL 원본 존재 여부만 본다 src/web-search/ollama-executor.ts의 runOllamaWebSearch - 검증되지 않은 endpoint로 제공자 apiKey를 Bearer로 보낸다 (redirect는 manual이라 리다이렉트 누수는 막혀 있음) src/lib/destination-policy.ts의 providerDestinationConfigError - baseUrl용 정책은 이미 있고 endpoint용 호출 경로만 없다
메인테이너의 판단이 필요한 지점
로컬 Ollama 루프백 endpoint를 허용할지: allowPrivateNetwork: true와 같은 명시 옵트인을 요구할지, 아니면 ollama 브리지에만 별도 허용 목록을 둘지
설정을 거절만 할지, 런타임(planPassthroughWebSearchBridge/runOllamaWebSearch 직전)에도 한 번 더 assert할지 (설정 우회·핫 리로드 대비)
#4515가 머지되기 전에 이 이슈를 먼저 고칠지, 아니면 #4515와 같은 브리지 문서/테스트 묶음에 넣을지 (기능적으로는 독립)
너의 추천 dev에서 바로 작은 수정 PR을 열어 providerWebSearchBridgeConfigError(또는 인접 검증)에 assessDestination/providerDestinationConfigError와 같은 분류를 붙이고, 메타데이터는 무조건 거절·사설/루프백은 명시 옵트인만 통과하게 하세요. 회귀 테스트에 http://169.254.169.254/와 사설 IP endpoint가 설정 단계에서 실패하는 케이스를 넣고, #4515와 섞지 말고 이 이슈만 닫히게 하세요.
webSearchBridge.endpoint no longer reaches the executor on a URL-shape check alone. The same classifier already guarding provider baseUrl now runs at both boundaries: providerWebSearchBridgeConfigError is called from src/server/auth-cors.ts:755 so a management write is rejected, and resolveOllamaWebSearchEndpoint calls providerDestinationConfigError at src/web-search/passthrough-bridge.ts:187 so a hand-edited config file cannot arm the bridge either. A refused endpoint returns undefined, which is what keeps the provider API key unspent — the executor is only constructed when a plan exists.
Metadata destinations always fail. Loopback, localhost and private space require the existing allowPrivateNetwork opt-in or a registry entry that is local by definition, so a local Ollama on 127.0.0.1 keeps working.
The runtime path is the load-bearing one, and that is deliberate rather than incidental: the config file load at src/config.ts:684 only schema-parses with .catch(undefined) and runs no error function, so a hand-edited file never reaches the config-time check. resolveOllamaWebSearchEndpoint is the only reader of this field in the tree, which is what lets it act as the authorization boundary.
An adversarial security review of the fix found one real defect before merge, now also fixed: a provider keyed under a custom name such as my-ollama with a loopback endpoint used to arm and would have silently disarmed, with no load error and a deliberately silent plan-time refusal. warnRefusedBridgeEndpointOnce at src/web-search/passthrough-bridge.ts:94 now emits one warning per provider-and-endpoint pair naming the remedy, omitting the URL and redacting the provider name.
Landed as #4555, merge commit 246b5cab432b03cbec1766c2faffac13d6e39321, verified by Cross-platform CI run 34781031241 at the exact merged head e8b36b0e202025780e84759542a78cb1488b2333.
One residual is carried knowingly and is not covered by this fix. A public hostname that resolves into private or metadata space passes a literal-only check, and unlike baseUrl this endpoint gets no asynchronous resolved-address check at management write. The plan-time boundary has to stay synchronous. That matches the project's existing DNS-rebinding residual rather than adding a new one, but it means this issue closes on the literal-destination hole, not on resolution-time protection.
Merged by admin on the project owner's explicit instruction without the second-maintainer security review MAINTAINERS.md asks for; that waiver is recorded on the PR.
Landed via #4555 at 246b5cab432b03cbec1766c2faffac13d6e39321.
webSearchBridge.endpoint now goes through the same destination assessment as provider baseUrl (providerDestinationConfigError / assessDestination). Metadata, loopback, and private endpoints refuse unless allowPrivateNetwork is set; a refused endpoint disarms the bridge and warns once without leaking the URL.
Verified on dev HEAD 246b5cab432b03cbec1766c2faffac13d6e39321 in src/web-search/passthrough-bridge.ts and src/config.ts.
Activity
lidge-jun commentedon Sep 13, 2026
리뷰 · 우선순위 74 / 80
설명
이 이슈는 운영자가
providers.<name>.webSearchBridge를 켤 때endpoint가 어디로 가는지 검사하지 않아서, 제공자baseUrl에는 이미 있는 목적지 정책이 브리지에는 빠진다는 보안 구멍 보고입니다. 현재devHEAD(d7c7b493b)를 기준으로 보면 보고 내용이 그대로 맞습니다.흐름을 초등학생도 따라갈 수 있게 풀면 이렇습니다. 웹 검색 브리지는 원래 Ollama Cloud로 검색을 대신 날리고, 그 자리에 모델이 쓸 검색 결과를 넣어 주는 옵션입니다. 이 기능을 켜려면 설정에
webSearchBridge.enabled: true와backend: "ollama"가 필요합니다. 이때 검색 요청을 어디로 보낼지는resolveOllamaWebSearchEndpoint가 정합니다. 운영자가endpoint를 직접 쓰면 그 URL을 그대로 쓰고, 안 쓰면baseUrl이 진짜 Ollama Cloud 원본일 때만 기본 경로를 붙입니다.문제는 그
endpoint를 받을 때입니다.src/config.ts의providerWebSearchBridgeConfigError는 URL이http/https로 파싱되는지만 봅니다. 같은 파일에서 제공자baseUrl은providerBaseUrlConfigError다음에providerDestinationConfigError로 루프백·사설망·클라우드 메타데이터(예:169.254.169.254)를 막습니다. 그런데 브리지endpoint에는 그 두 번째 검사가 없습니다. 주석에도 "여기선 모양만 보고, 키를 보내기 전에 다시 검사한다"고 적혀 있지만, 실제로planPassthroughWebSearchBridge가 다시 하는 일은originOf로 URL이 깨지지 않았는지만 보는 수준입니다. 목적지 분류(assessDestination)는 호출되지 않습니다.그다음
runOllamaWebSearch는 그endpoint로Authorization: Bearer <제공자 apiKey>를 붙여 POST합니다. 그래서 운영자가 실수로(또는 악의적 설정으로)http://169.254.169.254/같은 주소를 넣으면, 그 주소로 제공자 API 키가 나갑니다. 클라이언트가 마음대로 바꿀 수 있는 값은 아니고, 운영자 설정이 필요합니다. 그래도baseUrl은 같은 이유로 이미 막고 있으니, 브리지만 빠지는 건 방어 깊이 구멍입니다.범위도 이슈 본문과 같습니다. 지금
dev에서planPassthroughWebSearchBridge는 여전히backend !== "ollama"면 바로 꺼집니다. 열려 있는 #4515가 다른 백엔드를 무장시켜도, 그쪽은 이슈가 말한 대로webSearchBridge.endpoint를 안 쓰고 각자 고정/사이드카/baseUrl경로를 탑니다. 그래서 이 구멍은 ollama 백엔드 + 명시endpoint에 국한되고, #4515에 얹어 설명할 문제가 아닙니다. 따로 고치는 게 맞습니다.고치는 방향도 이미 레포 안에 있습니다.
providerDestinationConfigError/assessDestination과 같은 분류를webSearchBridge.endpoint에 설정 검증 시점에 돌리면, 쓰기 경계에서 거절됩니다. 로컬 Ollama를 루프백으로 쓰고 싶다면allowPrivateNetwork처럼 명시 옵트인이 필요한지 메인테이너가 정하면 됩니다. 메타데이터 주소는 옵트인 없이 계속 막아야 합니다.src/config.ts-providerWebSearchBridgeConfigError가 endpoint를 http(s) 파싱만 하고providerDestinationConfigError/assessDestination을 호출하지 않는다src/web-search/passthrough-bridge.ts의resolveOllamaWebSearchEndpoint- 설정된 endpoint를originOf통과만 하면 그대로 반환해 메타데이터/사설망을 거르지 않는다src/web-search/passthrough-bridge.ts의planPassthroughWebSearchBridge- 주석의 "키 보내기 전 재검증"이 목적지 정책이 아니라 URL 원본 존재 여부만 본다src/web-search/ollama-executor.ts의runOllamaWebSearch- 검증되지 않은 endpoint로 제공자apiKey를 Bearer로 보낸다 (redirect는 manual이라 리다이렉트 누수는 막혀 있음)src/lib/destination-policy.ts의providerDestinationConfigError- baseUrl용 정책은 이미 있고 endpoint용 호출 경로만 없다메인테이너의 판단이 필요한 지점
allowPrivateNetwork: true와 같은 명시 옵트인을 요구할지, 아니면 ollama 브리지에만 별도 허용 목록을 둘지planPassthroughWebSearchBridge/runOllamaWebSearch직전)에도 한 번 더 assert할지 (설정 우회·핫 리로드 대비)너의 추천
dev에서 바로 작은 수정 PR을 열어providerWebSearchBridgeConfigError(또는 인접 검증)에assessDestination/providerDestinationConfigError와 같은 분류를 붙이고, 메타데이터는 무조건 거절·사설/루프백은 명시 옵트인만 통과하게 하세요. 회귀 테스트에http://169.254.169.254/와 사설 IP endpoint가 설정 단계에서 실패하는 케이스를 넣고, #4515와 섞지 말고 이 이슈만 닫히게 하세요.이 댓글은 grok-bot이 작성했습니다
Merge pull request #4515 from lidge-jun/codex/260913-4429-passthrough…
docs(devlog): record the triage unit's post-delivery state (#4559)
fix(web-search): assess the bridge search endpoint as a destination (#…
lidge-jun commentedon Sep 13, 2026
Fixed on
devat246b5cab43.webSearchBridge.endpointno longer reaches the executor on a URL-shape check alone. The same classifier already guarding providerbaseUrlnow runs at both boundaries:providerWebSearchBridgeConfigErroris called fromsrc/server/auth-cors.ts:755 so a management write is rejected, andresolveOllamaWebSearchEndpointcallsproviderDestinationConfigErroratsrc/web-search/passthrough-bridge.ts:187 so a hand-edited config file cannot arm the bridge either. A refused endpoint returnsundefined, which is what keeps the provider API key unspent — the executor is only constructed when a plan exists.Metadata destinations always fail. Loopback, localhost and private space require the existing
allowPrivateNetworkopt-in or a registry entry that is local by definition, so a local Ollama on127.0.0.1keeps working.The runtime path is the load-bearing one, and that is deliberate rather than incidental: the config file load at
src/config.ts:684 only schema-parses with.catch(undefined)and runs no error function, so a hand-edited file never reaches the config-time check.resolveOllamaWebSearchEndpointis the only reader of this field in the tree, which is what lets it act as the authorization boundary.An adversarial security review of the fix found one real defect before merge, now also fixed: a provider keyed under a custom name such as
my-ollamawith a loopback endpoint used to arm and would have silently disarmed, with no load error and a deliberately silent plan-time refusal.warnRefusedBridgeEndpointOnceatsrc/web-search/passthrough-bridge.ts:94 now emits one warning per provider-and-endpoint pair naming the remedy, omitting the URL and redacting the provider name.Landed as #4555, merge commit
246b5cab432b03cbec1766c2faffac13d6e39321, verified by Cross-platform CI run 34781031241 at the exact merged heade8b36b0e202025780e84759542a78cb1488b2333.One residual is carried knowingly and is not covered by this fix. A public hostname that resolves into private or metadata space passes a literal-only check, and unlike
baseUrlthis endpoint gets no asynchronous resolved-address check at management write. The plan-time boundary has to stay synchronous. That matches the project's existing DNS-rebinding residual rather than adding a new one, but it means this issue closes on the literal-destination hole, not on resolution-time protection.Merged by admin on the project owner's explicit instruction without the second-maintainer security review MAINTAINERS.md asks for; that waiver is recorded on the PR.
lidge-jun commentedon Sep 13, 2026
Landed via #4555 at
246b5cab432b03cbec1766c2faffac13d6e39321.webSearchBridge.endpointnow goes through the same destination assessment as providerbaseUrl(providerDestinationConfigError/assessDestination). Metadata, loopback, and private endpoints refuse unlessallowPrivateNetworkis set; a refused endpoint disarms the bridge and warns once without leaking the URL.Verified on
devHEAD246b5cab432b03cbec1766c2faffac13d6e39321insrc/web-search/passthrough-bridge.tsandsrc/config.ts.이 댓글은 grok-bot이 작성했습니다
Merge pull request lidge-jun#4515 from lidge-jun/codex/260913-4429-pa…
docs(devlog): record the triage unit's post-delivery state (lidge-jun…
fix(web-search): assess the bridge search endpoint as a destination (l…