mirror of
https://github.com/ComposioHQ/composio.git
synced 2026-09-22 11:46:35 +08:00
6ba9179b48
This PR: - builds on top of https://github.com/ComposioHQ/composio/pull/4144 - routes every fetch whose URL comes from an API response through the SSRF guards that already existed — ten sinks, five per SDK: the tool-execution download, both S3 presigned uploads, `RemoteFile.buffer()`/`blob()`, and the Tool Router session file upload - adds `safe_request()` (Python), which follows redirects itself and re-validates each hop, so a validated URL cannot 302 into private space and an S3 307 region redirect still works - makes `RemoteFile.buffer()` (Python) share `_fetch_url_bytes` with the user-supplied-URL path instead of duplicating it — it previously read `response.content` with no size cap, no redirect control, and no target validation - adds `ssrfSafeFetchWhereSupported` (TypeScript): the full guard on Node, a plain `fetch` on workerd, so Tool Router session file transfers keep working in edge runtimes rather than failing closed - documents DNS rebinding as a known residual in both guards - adds tests that assert the guard *runs* — blocked URL, sink never called, nothing written — rather than that a transfer succeeds ## Context `python/AGENTS.md` states the trust boundary: every field of an API response is untrusted input, because the backend may be compromised or the connection MITM'd. Both SDKs enforced that for URLs a *user* passes in (`composio.utils.url_safety`, `ssrfGuard.node.ts`) and left the URLs an API *response* supplies unguarded — backwards relative to the stated model. A response naming an internal address turned the SDK into a request proxy for it, and the fetched bytes were written to disk or returned to the caller, typically into an LLM context. `RemoteFile.buffer()` was the weakest of the ten: four lines above it, `_fetch_from_url` validated its target, refused redirects, and streamed against a 100 MiB cap; `buffer()` did none of the three. The only difference between them was which side of the trust boundary the URL arrived from. Three decisions worth review: - **The guard is unconditional.** Presigned URLs are public, so nothing legitimate should resolve to private space. There is no escape hatch, and no new configuration surface. - **The tool-execution download stays uncapped.** It streams straight to disk, so `_MAX_RESPONSE_SIZE` — a memory-exhaustion bound — does not apply, and tool attachments legitimately exceed it. `RemoteFile.buffer()` *is* capped, because it buffers in memory. - **Uploads follow redirects with re-validation rather than refusing them**, since S3 can answer a PUT with a 307 region redirect. Downloads keep `allow_redirects=False`, matching the existing fetch paths. Python now matches the TypeScript guard's per-hop re-validation, which was the better of the two implementations. Verified locally: `make chk` and `make tst` clean on the Python side; `pnpm -C packages/core test` 1095 passed, typecheck and lint clean.
Changesets
Hello and welcome! This folder has been automatically generated by @changesets/cli, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it in our repository
We have a quick list of common questions to get you started engaging with this project in our documentation