Skip to content

security: re-validate redirects on the remote media path (3.9.2) - #19

Merged
BenKalsky merged 1 commit into
mainfrom
fix/media-redirect-ssrf
Sep 12, 2026
Merged

security: re-validate redirects on the remote media path (3.9.2)#19
BenKalsky merged 1 commit into
mainfrom
fix/media-redirect-ssrf

Conversation

@BenKalsky

Copy link
Copy Markdown
Member

From the ClawHub audit of 3.9.1. One real finding, reported independently by both AIG and ClawScan.

The bug

fetch_https_media validated the URL the caller supplied and then used urllib's default redirect handling:

validate_remote_url(url)
request = urllib.request.Request(url, headers={...})
response = urllib.request.urlopen(request, timeout=timeout)   # follows anywhere

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 SSRF validate_remote_url exists 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 by fetch_https_media()." #17 fixed the audit path and the authenticated path and left the third one on a bare urlopen.

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 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 — verified to fail with the fix reverted.

Two hardening notes from the same audit

  • [EA4] A request with no explicit timeout used urllib's default, which is no timeout. Now bounded by 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.
  • [SC1] requests gains 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_INSTALL at severity CRITICAL, 24 issues. Its four HIGH findings are text matches, and two of them fire on this skill's own security comments:

Finding What it matched
SSRF, security.py:182 the 169.254.169.254 in the docstring explaining the SSRF defence
Anti-Refusal, security.py:297 the phrase "never warn" in the docstring explaining the localhost exemption
Tool Misuse, api-reference.md:17 the line DELETE /posts/{id} in a REST API reference table
Data Exfiltration, wp_cli.py:108 os.environ.copy(), used to pass credentials to a child process instead of argv

ClawScan 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 suspicious is 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

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>
@BenKalsky

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 12, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-12T20:35:14.379745Z 5ef54cc Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

Reviewed commit: 5ef54cc90e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

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".

@BenKalsky
BenKalsky merged commit b3b7331 into main Sep 12, 2026
3 checks passed
@BenKalsky
BenKalsky deleted the fix/media-redirect-ssrf branch September 12, 2026 20:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant