fix(gateway): bound inbound media download size in the cache helpers - #42931
fix(gateway): bound inbound media download size in the cache helpers#42931youngstar-eth wants to merge 1 commit into
Conversation
|
Positive verification — clean size-bound guard for inbound media downloads.
Observations:
No issues found. |
5750deb to
a6557e6
Compare
|
Positive verification — inbound media DoS prevention ✅ Reviewed the full diff. The The ValueError is raised before Both |
|
Thanks for the pointer — yes, this addresses #13145, and I see the overlap with #13341 (configurable 128 MiB cap). On the cap value: I chose 50 MiB deliberately to match the existing Whichever direction reconciles best with #13341 works for me. |
egilewski
left a comment
There was a problem hiding this comment.
Recommendation: request changes
I reviewed this against current GitHub main d1383a6b1450c6c139720b1b01f8b99cc130453f and PR head a6557e6025865d5801a467d5d95959e0b1f02f25.
Validation:
git rev-list --left-right --count upstream/main...refs/remotes/upstream/pr/42931=>125 1;git merge-tree --write-tree upstream/main refs/remotes/upstream/pr/42931wrote treeba4f27f3f87df31a8be6bd6b97e10c5942947630;git diff --check upstream/main...refs/remotes/upstream/pr/42931passed.- GitHub checks were all successful or skipped at final recheck.
python -B -m pytest -q tests/gateway/test_media_download_retry.py -p no:cacheproviderpassed: 40 tests.python -B -m py_compile gateway/platforms/base.py tests/gateway/test_media_download_retry.pypassed.- Source inspection confirmed the protected call sites include the inbound URL paths in WhatsApp
mediaUrls, Signal, Feishu, and BlueBubbles viacache_image_from_url/cache_audio_from_url.
Finding:
The fix still does not bound memory for responses without a trustworthy Content-Length. Both cache_image_from_url() and cache_audio_from_url() call await client.get(...), then _enforce_media_size_limit() reads response.content. With httpx's normal request path, the full response body has already been buffered before response.content can be length-checked. That means a sender-controlled URL can omit Content-Length (or send a small/invalid one) and still force the gateway to buffer an arbitrarily large body in memory; the PR only prevents the oversized body from being written to the cache afterward.
Please enforce the limit while reading the response, for example by using client.stream(...) / aiter_bytes() and aborting once accumulated bytes exceed the cap, while keeping the early header rejection for honest oversized responses. The current tests pass because they inject a prebuilt resp.content; they do not exercise the no-length streaming case that still causes the memory-spike part of #13145.
Review stopped at this blocker; there may be other issues.
Signed: GPT-5.5-xhigh in Codex
cache_image_from_url and cache_audio_from_url read the full body via client.get() before any size check, so a sender-controlled URL (inbound platform media: WhatsApp mediaUrls, Signal/Feishu/BlueBubbles attachments) could omit or under-report Content-Length and still force the gateway to buffer an arbitrarily large body in memory — the memory-spike half of NousResearch#13145. A header pre-check plus a post-buffer length check does not prevent this because the body is already fully materialised. Read the body incrementally with client.stream(...) + aiter_bytes() and abort as soon as the accumulated size exceeds the 50 MiB cap (matching tools/vision_tools._VISION_MAX_DOWNLOAD_BYTES), keeping the Content-Length header pre-check for honest oversized responses. An unbounded body is never fully buffered now. The SSRF redirect guard (response event hook) and the retry/backoff behaviour are unchanged. Adds a regression test for the no-Content-Length streaming case (the gap a header + post-buffer check misses); existing tests updated to the streaming interface. Addresses review feedback on NousResearch#42931. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks @egilewski — you're right, and I've pushed a fix. The previous version checked Updated approach (
Tests: added Rebased onto current |
a6557e6 to
aa41ec8
Compare
egilewski
left a comment
There was a problem hiding this comment.
Recommendation: approve
I reviewed this against current GitHub main d62979a6f34f64f2ed840f159aac66e24d7cad78, PR base 955fa40062874faed1108f831864d29724a6250c, and PR head aa41ec8d8ab5c4243177b85fe2c867c628a2e7eb.
Validation:
git merge-tree --write-tree upstream/main upstream/pr/42931: passed, produced990326652c117cdbb7f659c8f4d140daeed9e09b.git diff --check upstream/main...upstream/pr/42931: passed./home/mac/hermes-agent/.venv/bin/python -B -m compileall -q gateway/platforms/base.py tests/gateway/test_media_download_retry.py: passed./home/mac/hermes-agent/.venv/bin/python -B -m pytest -o addopts='' -p no:cacheprovider tests/gateway/test_media_download_retry.py -q: passed,41 passed.- Direct capped-stream probe with
_MAX_MEDIA_DOWNLOAD_BYTES=64: raisedValueErrorafter consuming 80 bytes, confirming the body is read incrementally and aborts just past the cap. coderabbit review --plain --base upstream/main --type committed: completed with one trivial optional negative-Content-Lengthhardening note; I do not consider it blocking because the stream read still enforces the cap and empty/invalid image bodies are rejected before caching.
Finding:
I did not find a blocker in the reviewed scope. The previous memory-spike issue is addressed: cache_image_from_url() and cache_audio_from_url() now use client.stream(...) and _read_capped_body(), preserving the fast Content-Length rejection while also aborting no-length or under-reported bodies during aiter_bytes() before caching.
Signed: GPT-5.5-xhigh in Codex
|
Closing in favor of #50321 — #50321 — which caps inbound media size across all three media types (image/audio/video) in the shared cache helpers. Your PR fixed the same |
What & why
cache_image_from_urlandcache_audio_from_urldidcache_*_from_bytes(response.content, ext)with no size limit. The URLs they download come from inbound platform message payloads (e.g. WhatsAppdata["mediaUrls"], Signal/Feishu/BlueBubbles attachment URLs), so a remote sender chooses the host. The 30s timeout bounds wall-clock, not response size, so a single message pointing at a very large file could buffer an unbounded body and persist it to the cache directory (memory spike / disk fill); the cache is only pruned on a 24h age cutoff.Add a 50 MiB cap (matching
tools/vision_tools._VISION_MAX_DOWNLOAD_BYTES) enforced via a shared_enforce_media_size_limit()helper: reject on an oversizedContent-Lengthheader and re-check the actual body length before caching. Oversized responses raiseValueError(not a retryable error) so they fail fast without being re-downloaded.Robustness / DoS-hardening fix.
How to test
Platforms
macOS (pure-Python;
httpx).🤖 Generated with Claude Code