Skip to content

fix(security): block SSRF to private/internal/metadata addresses (closes #41)#42

Merged
syswave-dev merged 8 commits into
mainfrom
fix/ssrf-issue-41
Jul 8, 2026
Merged

fix(security): block SSRF to private/internal/metadata addresses (closes #41)#42
syswave-dev merged 8 commits into
mainfrom
fix/ssrf-issue-41

Conversation

@syswave-dev

Copy link
Copy Markdown
Collaborator

Closes #41.

Problem

The URL-fetch endpoints (GET /api, MCP read_url) performed server-side fetches of any user-supplied URL with no address validation, allowing SSRF against internal and cloud-metadata endpoints (e.g. http://100.100.100.200/latest/user-data, http://169.254.169.254/).

Fix

A new guard (lib/ssrf.js) that, by default, denies requests whose resolved address falls in a private, loopback, link-local, CGNAT, or cloud-metadata range:

  • Resolves the host and checks every returned address (IPv4 + IPv6), failing closed on resolution errors, empty results, and non-http(s) schemes.
  • Blocks the same private/metadata ranges reached via IPv6 transition addressing (NAT64 64:ff9b::/96, 6to4 2002::/16, IPv4-compatible/-mapped) by matching the embedded IPv4, so legitimate public destinations still resolve on NAT64 hosts.
  • Re-validates every redirect hop (redirect: 'manual'), so a public URL cannot 302 into an internal one.
  • Wired into all three user-controlled fetch paths: the main web fetch (also covers the Reddit/HN web fallback), Reddit short-link redirect resolution, and the Playwright sidecar dispatch (validated Node-side before the browser resolves DNS itself).
  • Blocked requests return HTTP 403 at /api (and an isError result at MCP read_url), checked before the cache lookup so a previously-cached internal response cannot be served.

Opt-out for self-hosters

Default-deny. Self-hosters who intentionally fetch internal hosts can allowlist specific CIDRs and/or hostnames via PULLMD_ALLOWED_HOSTS (e.g. 10.0.5.0/24,wiki.internal). There is no global kill-switch.

Known residuals (documented)

  • DNS rebinding: the guard resolves-and-checks but does not pin the socket to the checked IP; full mitigation (a connection-pinning dispatcher) is a possible follow-up.
  • Playwright internal navigation: once an allowed page loads, client-side redirects/fetches inside the headless browser are not Node-guarded; container egress rules are the second layer.
  • Outbound HTTP proxy: if egress is routed through a proxy, the proxy's egress rules are authoritative (the guard validates the intended target's resolved IPs but cannot pin past the proxy).

Testing

  • New unit tests for the guard (IP ranges incl. NAT64/6to4, redirect-hop re-validation, allowlist parsing, scheme rejection).
  • New endpoint tests: blocked URLs return 403 at /api and isError at MCP read_url.
  • Full suite: 794/794 passing. Verified end-to-end against a running server: the reported payload returns 403; normal public URLs are unaffected.

Version bumped to 3.3.0 with a CHANGELOG Security entry and PULLMD_ALLOWED_HOSTS documented in .env.example and the help page.

🤖 Generated with Claude Code

https://claude.ai/code/session_013pFhk7BB4SZ8dNBLQbsiQL

syswave-dev and others added 8 commits July 8, 2026 16:04
…idate allowlist CIDR prefix

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013pFhk7BB4SZ8dNBLQbsiQL
@syswave-dev syswave-dev merged commit 9644889 into main Jul 8, 2026
4 checks passed
@syswave-dev syswave-dev deleted the fix/ssrf-issue-41 branch July 8, 2026 15:27
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.

[Security Vulnerability] Server-Side Request Forgery (SSRF) allows unauthorized access to cloud metadata and sensitive information

1 participant