fix(relayer): resolve client IP via trusted proxy hops to stop X-Forwarded-For spoofing (#360)#361
Open
ducnmm wants to merge 1 commit into
Open
fix(relayer): resolve client IP via trusted proxy hops to stop X-Forwarded-For spoofing (#360)#361ducnmm wants to merge 1 commit into
ducnmm wants to merge 1 commit into
Conversation
…arded-For spoofing (#360) The MCP SSE/messages/streamable proxy routes intentionally skip signed-request auth and per-key rate limiting, so the per-IP session cap in the Node sidecar is the only per-source defense against an unauthenticated session flood. Both the sidecar limiter and the /sponsor IP limiter derived the client IP by taking the FIRST (leftmost) value of X-Forwarded-For, which is fully client-controlled — a single host could send a fresh fake XFF each request and land in a new per-IP bucket, bypassing maxSessionsPerIp / maxNewSessionsPerIpPerMin. Resolve the real client IP by counting a configured number of trusted reverse proxies in from the socket end (express-style `trust proxy` semantics) and never trusting the leftmost value: - New shared `client_ip::resolve_client_ip(xff, peer, trusted_hops)` helper with unit tests (spoof-defeat, 0/1/2 hops, IPv6, misconfig clamp). - New `TRUSTED_PROXY_HOPS` config (default 1 = Railway edge; 0 = directly exposed, ignore XFF). Documented in .env.example. - mcp_proxy `inject_forwarded_for` now forwards a single sanitized client IP to the sidecar instead of appending peer to a client-controlled chain. - sponsor_rate_limit_middleware uses the same resolver. - Sidecar `clientIpFromRequest` takes the rightmost (nearest-relayer) hop as defense-in-depth and documents the trust boundary.
harrymove-ctrl
approved these changes
Jul 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #360.
The MCP proxy routes (
/api/mcp/sse,/api/mcp/messages,/api/mcp) intentionally skip signed-request auth and per-key rate limiting, so the per-IP session cap in the Node sidecar is the only per-source defense against an unauthenticated session flood. Both that limiter and the/sponsorIP limiter derived the client IP from the first (leftmost) value ofX-Forwarded-For— which is fully client-controlled. A single host could send a fresh fakeX-Forwarded-Foron each request, land in a brand-new per-IP bucket every time, and sail pastmaxSessionsPerIp/maxNewSessionsPerIpPerMin.This PR resolves the real client IP by counting a configured number of trusted reverse proxies in from the socket end (express-style
trust proxysemantics) and never trusting the leftmost value.Changes
client_ip::resolve_client_ip(xff, peer, trusted_hops)— new shared helper. Builds the address chain[socket_peer, XFF reversed…], skipstrusted_hopstrusted proxies, and returns the first address the client cannot forge past. Clamps a too-large hop count instead of underflowing. Unit-tested (spoof-defeat, 0/1/2 hops, IPv6, misconfig).TRUSTED_PROXY_HOPS— new config (default1= Railway's single edge proxy;0= relayer directly exposed → ignoreX-Forwarded-For). Documented in.env.example.mcp_proxy::inject_forwarded_for— now forwards one sanitized client IP to the sidecar instead of appending the peer to a client-controlled chain. The sidecar is loopback-only from the relayer, so that single value is authoritative.sponsor_rate_limit_middleware— uses the same resolver (secondary route; per-sender limit already backstops it, but fixed for consistency / defense-in-depth).clientIpFromRequest— takes the rightmost (nearest-relayer) hop as defense-in-depth and documents the trust boundary.TRUSTED_PROXY_HOPS=1, correct for the current Railway topology (one edge proxy). Any environment that exposes the relayer without a proxy in front must setTRUSTED_PROXY_HOPS=0; a value larger than the real hop count re-opens the spoofing hole.Testing
cargo test—client_ip10/10,mcp_proxyinject 5/5,rate_limit26/26 pass.cargo clippy— no new warnings.Scope is entirely relayer + sidecar; the Move contract is untouched.