Files
pad/cmd
xarmian 22bfe8cc89 feat(mcp): off-cloud Host allowlist on the non-API MCP paths (TASK-2319, PLAN-2310 U3) (#1665)
* feat(mcp): off-cloud Host allowlist on the non-API MCP paths (TASK-2319, PLAN-2310 U3)

Off cloud, /mcp, /oauth/* and /.well-known/oauth-* answer 421
Misdirected Request (JSON) when Host does not name the configured
origin, MCP URL or auth-server URL. Matching is case-insensitive with
the port defaulted from the configured scheme; a loopback origin admits
every loopback spelling on its port; an empty Host is refused. It runs
after the availability gate (MCP off still answers 404 whatever the
Host) and before auth, audit and the OAuth group's rate limiter. The
/api/v1 routes are not wrapped: they keep the API perimeter, and the
in-process dispatcher sends them an empty Host. Cloud is unchanged.
mcp-go's DisableLocalhostProtection stays, with its comment rewritten.
The e2e harness sets PAD_URL to its base URL.

Claude-Session: https://claude.ai/code/session_016PXRzXhokr61CLbbu3Txj9

* feat(mcp): honour X-Forwarded-Host from trusted proxies; 421 names the fix (TASK-2319 lead review)

Behind a proxy that rewrites Host to the backend's name (Azure
Application Gateway and App Service, some ingresses) every MCP request
would get a 421. The allowlist now checks the first X-Forwarded-Host
entry when the TCP peer is in PAD_TRUSTED_PROXIES, the same trust test
cliAuthScheme and TrustedProxyRealIP use; from any other peer the header
is ignored, so DNS-rebinding protection is unchanged. The 421 names the
received host and its header, the configured origin, and the proxy fix,
and the first refusal per host is logged at WARN behind one overall
limiter (5, then 1 a minute; at most 1024 hosts remembered).

Claude-Session: https://claude.ai/code/session_016PXRzXhokr61CLbbu3Txj9
2026-09-29 15:25:44 -04:00
..