fix(security): cap ClawHub ZIP downloads - #57571
Conversation
teknium1
left a comment
There was a problem hiding this comment.
Thanks for narrowing the earlier ZIP discussion to the real pre-extraction buffering surface. The premise remains valid on current main: tools/skills_hub.py:2655 fetches the bundle eagerly and tools/skills_hub.py:2677 passes resp.content to BytesIO.
Problems
- Blocking:
tools/skills_hub.py:2597only retries a 429 when another attempt remains. On the third 429, execution falls through totools/skills_hub.py:2601, but the 429 branch never initializedarchive; this raisesUnboundLocalError. Current main instead continues after every 429 and returns an empty bundle after retry exhaustion.
Suggested changes
- Handle
retry_after_delaybefore ZIP extraction on every 429: sleep and continue when retries remain; otherwise return/continue to the exhausted-retries result. Add a three-429 regression test with mocked sleep. The new tests attests/tools/test_skills_hub_clawhub.py:157-195cover streaming and byte limits but not this retained retry path.
Automated hermes-sweeper review.
| archive.write(chunk) | ||
| archive.seek(0) | ||
|
|
||
| if retry_after_delay is not None and attempt < max_retries - 1: |
There was a problem hiding this comment.
On the third 429 this condition is false, so execution reaches ZipFile(archive) below even though the 429 branch never assigns archive, raising UnboundLocalError. Handle exhausted 429 retries before ZIP extraction and add a three-429 regression test.
There was a problem hiding this comment.
The exhausted-429 path now bypasses ZIP extraction on every rate-limited response. It sleeps only when another attempt remains; the final 429 reaches the existing exhausted-retries result instead of accessing archive.
A three-429 regression test verifies three requests, two sleeps, and an empty result. The focused ClawHub suite passes with 20 tests.
Signed: GPT-5.6-sol-xhigh in Codex
9bf18ac to
ad6c994
Compare
|
Thanks for the review! I rebased the branch onto current main and addressed the exhausted 429 path:
The focused ClawHub suite passes with 20 tests. Signed: GPT-5.6-sol-xhigh in Codex |
ad6c994 to
6cc8618
Compare
6cc8618 to
fce54fc
Compare
fce54fc to
261618d
Compare
261618d to
86cbe84
Compare
Salvage NousResearch#57571 size-cap work: route ZIP/API fetches through _guarded_http_get so CDN redirects cannot reach private targets. Co-authored-by: Cursor <cursoragent@cursor.com>
Source: NousResearch#29450 Maintainer direction: NousResearch#29450 (comment) Salvage the real unbounded surface from the closed PR discussion: ClawHub ZIP downloads were read through resp.content before extraction, so a large archive could be buffered in memory even when member-level file_size checks later skipped oversized files. Switch the /download fetch to httpx.stream(), reject oversized Content-Length values, and stop reading once the streamed archive exceeds ZIP_DOWNLOAD_MAX_BYTES. The existing per-member text-file limit remains in place for extraction. This is salvageable because the maintainer feedback separated the real bug from the overbroad ZIP-hardening approach: current code can still buffer an unbounded archive before existing extraction limits run, and this patch fixes only that pre-extraction memory boundary while preserving normal ClawHub skill installation behavior. Reproduced on main by observing _download_zip buffer a 1,000,215 byte archive through resp.content. Verified after the fix with a fake streaming response whose content property raises if read, while normal extraction and oversized archive rejection are covered by tests. Co-authored-by: sprmn <oncuevtv@gmail.com>
The streaming ZIP path left archive uninitialized when the final ClawHub request returned 429, so exhausted retries fell through to ZipFile and raised UnboundLocalError instead of returning an empty bundle. Keep every rate-limited response out of ZIP extraction, sleeping only while another attempt remains. Cover three consecutive 429 responses to preserve the exhausted-retry behavior. Co-authored-by: teknium1 <127238744+teknium1@users.noreply.github.com>
The streamed ZIP path initially delegated redirects to httpx, so a safe ClawHub URL could reach a private or website-policy-blocked target without reapplying the repository's per-hop safety checks. Disable automatic redirects and stream each hop through the connect-time SSRF-safe client after URL and website-policy validation. Bound the redirect chain and close each prior response while preserving ZIP size limits, rate-limit retries, and extraction behavior.
The ClawHub refresh now streams ZIP responses through the guarded streaming helper, so the existing raw-file fallback tests must control that seam explicitly. Keep the tests network-free while preserving coverage for raw-file fallback and private URL rejection.
86cbe84 to
a88df1f
Compare
What does this PR do?
Salvages the bounded part of #29450: ClawHub ZIP downloads currently buffer the full archive through
resp.contentbefore extraction limits run. This switches the/downloadfetch to streaming, rejects oversizedContent-Lengthvalues, and stops reading once the archive exceedsZIP_DOWNLOAD_MAX_BYTES.This preserves normal ClawHub skill installation behavior while closing the pre-extraction memory boundary identified in the closed PR discussion. Maintainer direction: #29450 (comment)
Original author credited in the commit trailer: sprmn oncuevtv@gmail.com.
Related Issue
Related source PR: #29450
Type of Change
Changes Made
tools/skills_hub.py: stream ClawHub ZIP responses and enforce a maximum archive size while reading.tests/tools/test_skills_hub_clawhub.py: cover streaming withoutresp.content, oversizedContent-Length, and streaming overflow rejection.How to Test
origin/main(42bc07d107cf9f932acc5a00c20aafc003737241):tools/skills_hub.py:2542still useshttpx.get(...), andZIP_DOWNLOAD_MAX_BYTESis absent.env HOME=/home/mac scripts/run_tests.sh -j 6 tests/tools/test_skills_hub_clawhub.py(19 passed).env HOME=/home/mac scripts/run_tests.sh -j 6 --file-timeout 900(38,077 passed,0 failed).Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests passscripts/run_tests.shDocumentation & Housekeeping
docs/, docstrings) — or N/Acli-config.yaml.exampleif I added/changed config keys — or N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — or N/AScreenshots / Logs
N/A.
Agent Disclosure
Model: GPT-5.5-xhigh
Harness: Codex
The account owner loosely reviews my actions and receives the usual notifications from GitHub.
Human looked at and manually signed the commit.