Skip to content

fix(integrations): pin api_call to the SSRF-validated IP#5727

Open
ashvinctrl wants to merge 1 commit into
odysseus-dev:devfrom
ashvinctrl:pin-apicall
Open

fix(integrations): pin api_call to the SSRF-validated IP#5727
ashvinctrl wants to merge 1 commit into
odysseus-dev:devfrom
ashvinctrl:pin-apicall

Conversation

@ashvinctrl

Copy link
Copy Markdown
Contributor

Summary

execute_api_call (src/integrations.py) runs check_outbound_url on the target before making the request, but that guard only resolves the host to answer (ok, reason) — it hands back no address. The request right after it opened a plain httpx.AsyncClient(), which resolves the host again at connect time. A base_url host on a low TTL can pass the guard as a public IP and then flip to 169.254.169.254 for the connect, so the call lands on cloud metadata with the integration's stored auth headers attached. That's the DNS-rebinding TOCTOU in #5513.

The fix resolves once, remembers the IPs the guard actually validated, and pins the client's socket to that set through a small AnyIO-backed transport. SNI and the Host header still come from the URL, so TLS and vhost routing are unchanged; connect-time fallback stays inside the approved address set over one shared deadline. This is the same pinning the webhook sender (src/webhook_manager.py) and web-fetch (services/search/content.py) paths already do — api_call was the last outbound path that skipped it.

Target branch

  • This PR targets dev, not main.

Linked Issue

Fixes #5513

Type of Change

  • Bug fix (non-breaking — fixes a confirmed issue)

Checklist

  • I searched open issues and open PRs — this is not a duplicate.
  • This PR targets dev
  • My changes are limited to the scope described above — no unrelated refactors or whitespace changes mixed in.
  • I actually ran the app (docker compose up or uvicorn app:app) and verified the change works end-to-end. Type-checks and unit tests are not enough.

How to Test

  1. python -m pytest tests/test_integration_api_call_ssrf.py tests/test_integrations_api_call_truncation.py — 21 pass. On dev (no pin), the 9 pin tests in the first file fail.
  2. Rebinding proof: stand up two loopback servers on the same port, one on 127.0.0.1 (the address the guard validates) and one on 127.0.0.2 (a stand-in for the metadata host). Point the guard's resolver at 127.0.0.1 and monkeypatch the connect-time resolver (anyio._core._sockets.getaddrinfo) to return 127.0.0.2, then call execute_api_call on a bearer integration. On dev the request lands on 127.0.0.2 and the Authorization header leaks; with this PR it stays on 127.0.0.1.
  3. Normal calls to a real integration still work — the pin only fixes the socket destination, not the hostname used for TLS/Host.

execute_api_call runs check_outbound_url on the target, but that guard only
resolves the host to answer (ok, reason) and hands back no address. The request
right after it opened a plain httpx.AsyncClient, which resolves the host again at
connect time. A base_url host on a low TTL can pass the guard as a public IP and
then flip to 169.254.169.254 for the connect, so the call lands on cloud metadata
with the integration's stored auth headers attached.

Resolve once, remember the IPs the guard actually validated, and pin the client's
socket to that set through a small AnyIO-backed transport. SNI and the Host header
still come from the URL, so TLS and vhost routing are unchanged; connect-time
fallback stays inside the approved address set over one shared deadline. This is
the same pinning the webhook sender and web-fetch paths already do -- api_call was
the last outbound path that skipped it.

Fixes odysseus-dev#5513
@github-actions github-actions Bot added the ready for review Description complete — ready for maintainer review label Jul 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready for review Description complete — ready for maintainer review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(security): integration api_call has a DNS-rebinding TOCTOU (validation resolves, connect re-resolves)

1 participant