Skip to content

[Bug]: webSearchBridge.endpoint skips destination policy, so a metadata-address endpoint receives the serving provider API key #4519

Closed

Description

@lidge-jun

Summary

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.

Activity

  1. added
    bugSomething isn't working
    toolstool_calls, MCP, web-search / sidecar tools
    on Sep 13, 2026
  2. lidge-jun commented on Sep 13, 2026

    @lidge-jun
    OwnerAuthor

    리뷰 · 우선순위 74 / 80

    설명
    이 이슈는 운영자가 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와 섞지 말고 이 이슈만 닫히게 하세요.

    이 댓글은 grok-bot이 작성했습니다

  3. lidge-jun commented on Sep 13, 2026

    @lidge-jun
    OwnerAuthor

    Fixed on dev at 246b5cab43.

    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.

  4. lidge-jun commented on Sep 13, 2026

    @lidge-jun
    OwnerAuthor

    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.

    이 댓글은 grok-bot이 작성했습니다

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinglanded-via-maintainerOriginal PR closed after landing via a maintainer merge traintoolstool_calls, MCP, web-search / sidecar tools

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions