security: re-validate redirects on the remote media path (3.9.2) - #19
Merged
Conversation
fetch_https_media validated the URL the caller supplied and then used urllib's default redirect handling, so a public HTTPS host could answer 302 http://127.0.0.1/ - or any private, loopback or link-local address - and the download would follow it, scheme downgrade included. That is precisely the SSRF validate_remote_url exists to prevent, and 3.9.0 built the right defence for the site audit while leaving this path on a bare urlopen. The redirect handler is now parameterised by the caller's validator rather than hardcoding one policy: the media opener re-validates against HTTPS-only and globally-reachable, the probe opener keeps its own http-permitting rule. A static test fails if security.py calls urllib.request.urlopen directly again - the module that owns the redirect rules must not be the one bypassing them. Two hardening notes from the same audit: a request with no explicit timeout is bounded by DEFAULT_REQUEST_TIMEOUT (300s) rather than urllib's default of none, and requests gains an upper bound (>=2.32.3,<3). An exact pin was considered and rejected: it would stop users receiving patch-level security fixes for the dependency whose advisory is the reason for the lower bound. Reported by AIG and ClawScan on the 3.9.1 audit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Member
Author
|
@codex review |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Codex Review: Didn't find any major issues. Keep it up! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
From the ClawHub audit of 3.9.1. One real finding, reported independently by both AIG and ClawScan.
The bug
fetch_https_mediavalidated the URL the caller supplied and then used urllib's default redirect handling:So a public HTTPS host could answer
302 http://127.0.0.1/— or any private, loopback or link-local address — and the download would follow it, scheme downgrade included. That is exactly the SSRFvalidate_remote_urlexists to prevent.AIG put it more pointedly than I can: "The project already implements the correct defensive pattern for unauthenticated site-audit requests in
_PublicHostRedirectHandler, which validates every redirect. That protection is not used byfetch_https_media()." #17 fixed the audit path and the authenticated path and left the third one on a bareurlopen.The fix
The redirect handler is now parameterised by the caller's validator instead of hardcoding one policy, because the rule genuinely differs: the audit may follow
http://(detecting a missing HTTPS redirect is one of its checks), a media download may not._MEDIA_OPENER→ re-validates every redirect against HTTPS-only + globally reachable_PROBE_OPENER→ keeps the http-permitting audit ruleA static test fails if
security.pycallsurllib.request.urlopendirectly again. The module that owns the redirect rules must not be the one bypassing them — verified to fail with the fix reverted.Two hardening notes from the same audit
DEFAULT_REQUEST_TIMEOUT(300s) — generous enough for a 10 MB upload on a slow link, but a hung connection can no longer stall an agent indefinitely. An explicit timeout still wins.requestsgains an upper bound:>=2.32.3,<3. I did not pin exactly, which is what the scanner asked for: an exact pin stops users receiving patch-level security fixes for the very dependency whose advisory is the reason for the lower bound. The major bound covers the breaking-change risk.SkillSpector, again
DO_NOT_INSTALLat severity CRITICAL, 24 issues. Its four HIGH findings are text matches, and two of them fire on this skill's own security comments:security.py:182169.254.169.254in the docstring explaining the SSRF defencesecurity.py:297api-reference.md:17DELETE /posts/{id}in a REST API reference tablewp_cli.py:108os.environ.copy(), used to pass credentials to a child process instead of argvClawScan evaluated all four and downgraded each with reasons. Writing the defence is what raised SkillSpector's count from 19 to 24 — the score tracks how much security-shaped text a file contains, not what the code does. Its verdict stays not credible for this skill; ClawScan's is the one worth reading, and its remaining
suspiciousis now about capability (live-site writes with real credentials), which is what this skill is for and what its guidance already says.Tests: 87 → 96.
🤖 Generated with Claude Code