fix(http): prevent DNS rebinding and SSRF in HTTP mode#30
Conversation
HTTP mode exposed /mcp and /sse with no Host/Origin validation, and fetch_l402 fetched any caller-supplied URL. Combined, a malicious web page could DNS-rebind to a victim's local MCP listener and read private HTTP resources via fetch_l402 (SSRF). - Add hostOriginGuard middleware validating Host/Origin on all HTTP endpoints (loopback-only by default; ALLOWED_HOSTS/ALLOWED_ORIGINS to extend). - Add assertPublicUrl SSRF guard to fetch_l402 that blocks loopback, link-local (incl. cloud metadata), RFC1918 and IPv6 local ranges; ALLOWED_FETCH_HOSTS opt-in for local development. - Set ALLOWED_HOSTS on the Fly deployment so mcp.getalby.com keeps working. Reported by Avishai Gonen (Pluto Security).
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
How is this exploitable in practice? the normal flow is that your agent can access a few alby MCP tools to communicate with your wallet. |
Summary
HTTP mode (
MODE=HTTP, incl. the hostedmcp.getalby.com) had two issues that chain into a private-network read:Host/Originvalidation on/mcp,/sse,/messages. A malicious web page can DNS-rebind its hostname to a loopback/private address and send browser-origin requests to a victim's local MCP listener.fetch_l402fetched any caller-supplied URL and returned the body — an SSRF sink. "Auth" was only anostr+walletconnect://prefix check, which an attacker can satisfy with a fake connection secret (no real wallet needed, since a 200 response never triggers a payment).Chained: rebind → reach local
/mcp→ fakenwcaccepted →fetch_l402reads e.g.http://127.0.0.1/internal/...→ response returned to the attacker page.Reported by Avishai Gonen (Pluto Security). Verified against
bee4ffc(@getalby/mcp@1.1.1).Changes
hostOriginGuardmiddleware (src/security.ts) on all HTTP endpoints. Loopback-only by default; extend viaALLOWED_HOSTS/ALLOWED_ORIGINS. Non-browser MCP clients (noOriginheader) are unaffected.assertPublicUrlSSRF guard infetch_l402. Allows onlyhttp(s)URLs resolving exclusively to globally-routable addresses; blocks loopback, link-local (incl.169.254.169.254), RFC1918, CGNAT and IPv6 local/ULA ranges, and unwraps IPv4-mapped IPv6.ALLOWED_FETCH_HOSTSis an opt-in escape hatch for local dev.fly.toml: setsALLOWED_HOSTS = 'mcp.getalby.com'so the hosted deployment keeps working.Testing
yarn buildpasses.assertPublicUrl: blocks127.0.0.1,[::1],169.254.169.254,10/8,192.168/16,::ffff:127.0.0.1,file:; allows public hosts; honorsALLOWED_FETCH_HOSTS.Hostpasses the guard; rebindingHost: evil.com→ 403; attackerOrigin→ 403.Known limitations / follow-ups
assertPublicUrlresolves and validates, butfetchWithL402re-resolves at fetch time — a rebind between the two could still slip through. The Host/Origin guard closes the primary browser vector; fully closing this needs pinning the validated IP at fetch time (would require changes in@getalby/lightning-tools).fetch_l402follows redirects, so a public URL could 30x to a private one. Considerredirect: "manual"+ re-validation.assertPublicUrltorequest_invoice(lightning-address lookup) as well.