Skip to content

fix(http): validate incoming Host/Origin and outbound fetch targets - #33

Draft
reneaaron wants to merge 1 commit into
masterfrom
fix/http-request-validation
Draft

fix(http): validate incoming Host/Origin and outbound fetch targets#33
reneaaron wants to merge 1 commit into
masterfrom
fix/http-request-validation

Conversation

@reneaaron

@reneaaron reneaaron commented Aug 24, 2026

Copy link
Copy Markdown
Member

Summary

Tightens what the HTTP transport accepts as an incoming request, and what fetch_l402 is willing to reach on the way out.

Changes

  • hostOriginGuard middleware (src/security.ts), applied to all HTTP endpoints (/mcp, /sse, /messages). A request is accepted only if its Host header is allowlisted and its Origin, when present, is explicitly permitted. The default allowlist is loopback only; ALLOWED_HOSTS replaces it, ALLOWED_ORIGINS permits browser origins and is empty by default. MCP clients that send no Origin header (i.e. everything that isn't a browser) are unaffected. 403 responses name the variable to set.
  • assertPublicUrl destination check in fetch_l402. Only http(s) URLs whose hostname resolves exclusively to globally-routable addresses are fetched; loopback, link-local, RFC1918, CGNAT and IPv6 local/ULA ranges are refused, and IPv4-mapped IPv6 is unwrapped before classification. Range classification uses ipaddr.js (already present transitively via expressproxy-addr) rather than string matching, so obfuscated forms like http://2130706433/ and http://0177.0.0.1/ are handled by validating the resolved address. There is no escape hatch — non-public addresses are refused unconditionally.
  • Redirects are refused (redirect: "error") rather than followed. A validated public URL could otherwise redirect to a private one, and since each hop is a separate fetchWithL402 call, each would independently pay an L402 challenge with no user confirmation.
  • fly.toml sets ALLOWED_HOSTS = 'mcp.getalby.com' so the hosted deployment keeps working.
  • README documents the two environment variables and the fetch behaviour.

Testing

tsc passes. 25 assertions run against the built code, driving the real fetch_l402 handler and the real middleware:

  • Refused: 127.0.0.1, [::1], localhost, 169.254.169.254, 10/8, 192.168/16, 100.64/10, [::ffff:127.0.0.1], [fd00::1], 2130706433, 0177.0.0.1, file:; ordinary public hosts allowed.
  • redirect: "error" is confirmed to reach fetch; a refused redirect surfaces a clear message rather than an opaque TypeError; ordinary public responses are unaffected.
  • Host normalization: bare ::1, [::1]:3000, and an ALLOWED_HOSTS entry carrying a port all match; a foreign Host is rejected; setting ALLOWED_HOSTS stops localhost being accepted.
  • Origin: absent is allowed, present-but-empty is rejected, foreign is rejected.

Notes

  • assertPublicUrl validates the resolved address, but the fetch resolves the hostname again independently. A DNS server that answers this check with a public address and the fetch with a private one therefore defeats it. Closing that means connecting to the validated address, which @getalby/lightning-tools cannot currently express.
  • This overlaps with feat: enable cors #28: that PR enables permissive CORS, while this guard rejects unlisted origins before CORS is consulted. Whichever lands second should reconcile the two, most likely by setting ALLOWED_ORIGINS to whatever feat: enable cors #28 intends to permit.

@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@reneaaron
reneaaron force-pushed the fix/http-request-validation branch 3 times, most recently from ce6fdda to 7dceef2 Compare August 25, 2026 09:29
Add a hostOriginGuard middleware to the HTTP endpoints (/mcp, /sse,
/messages). It accepts a request only if its Host header is allowlisted
and its Origin, when present, is explicitly permitted. The default
allowlist is loopback only; ALLOWED_HOSTS replaces it and ALLOWED_ORIGINS
permits browser origins. Clients that send no Origin header at all, which
is every non-browser MCP client, are unaffected.

Bind the HTTP listener to loopback by default instead of every interface,
so it is not reachable from the rest of the network. BIND_HOST opts a
deployment back into binding all interfaces.

Validate the destination of every fetch_l402 request: only http(s) URLs
that resolve exclusively to globally-routable addresses are fetched, so
loopback, link-local, RFC1918, CGNAT and IPv6 local ranges are refused.
The hostname is resolved rather than pattern-matched, so a public name
pointing at a private address is refused too. Redirects are not followed,
since a validated URL could otherwise redirect to a private one and each
hop would be a separate L402 challenge paid without the user seeing it.

Set ALLOWED_HOSTS and BIND_HOST on the Fly deployment so mcp.getalby.com
keeps working.
@reneaaron
reneaaron force-pushed the fix/http-request-validation branch from 7dceef2 to c9e0982 Compare August 25, 2026 21:21
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