{
  "question": "Our MCP endpoint only accepts calls from a short list of IP addresses, as a second layer on top of tokens. Could a caller from anywhere get past that list just by sending an X-Forwarded-For header with an allowed address, and how do we check that our allowlist, and any API key setting, is actually enforced rather than only configured?",
  "slug": "can-a-spoofed-x-forwarded-for-header-get-past-an-mcp-servers-ip-allowlist",
  "url": "https://www.rightshold.com/answers/can-a-spoofed-x-forwarded-for-header-get-past-an-mcp-servers-ip-allowlist",
  "short_answer": "Yes, if the server reads the client address from X-Forwarded-For without checking who added it. GHSA-wchh-mrv2-5h4q (CVE-2026-86041, added to GitHub's advisory database November 9, 2026) shows it in ezBookkeeping: its MCP and API-token IP allowlists (mcp_allowed_remote_ips, api_token_allowed_remote_ips) matched against Gin's ClientIP(), and because the router never called SetTrustedProxies, Gin trusted every peer's forwarding headers. One header, X-Forwarded-For: <an allowed IP>, got a request from any address through the gate, and the same forged address went into the access log and each transaction's CreatedIp, so the records pointed at the trusted host. Tokens were still required; the advisory says the bypass removes the compensating IP control, not the token check. ezBookkeeping 1.6.0 accepts X-Forwarded-For, X-Real-IP and X-Real-Port only from configured trusted proxies, or from the private ranges when none are configured. The rule from MDN's header reference: use only addresses your own proxies added, read from the right, and if the server can be reached directly from the internet, trust no part of the header. A related lesson came on October 29, 2026: @sylphx/pdf-reader-mcp logged \"API key authentication enabled\" while never checking the key (CVE-2026-62264), so test every control from outside instead of trusting the config or the log line.",
  "checklist": [
    "Find where your server gets the client address for allowlists, rate limits and logs. If it reads X-Forwarded-For, X-Real-IP or Forwarded, find out which peers it trusts to send them.",
    "In Gin, call SetTrustedProxies with your proxy's addresses, or SetTrustedProxies(nil) if there is no proxy. In other frameworks, find the equivalent trust-proxy setting and set it to your actual proxy hops, not to everything.",
    "Pick the client address from the right of the X-Forwarded-For list, skipping only your own proxies' addresses, and treat several X-Forwarded-For headers as one combined list.",
    "Make the server reachable only through the proxy: firewall the port or bind it to the private interface the proxy uses. If a client can connect directly, its own headers are untrusted.",
    "If you rely on a trusted private range, remember that other hosts and containers on that network can send the header too; narrow it to the proxy's address where you can.",
    "Log the connecting peer address next to the derived client address, so a forged header shows up as a mismatch instead of rewriting your audit trail.",
    "Treat an IP allowlist as a second layer only. Keep token or OAuth checks on every call, because an address can be shared, reassigned or forged.",
    "Test it from outside: call the endpoint from an address that isn't on the list, with and without X-Forwarded-For set to an allowed address, and with no credential. Expect a refusal every time."
  ],
  "sections": [
    {
      "heading": "What the ezBookkeeping advisory showed",
      "points": [
        "ezBookkeeping, a self-hosted personal finance app with a built-in MCP endpoint, lets operators limit its MCP endpoint and API tokens to listed source addresses. The advisory says the web server built its Gin router with gin.New() and never called SetTrustedProxies, so Gin treated every peer as a trusted proxy and ClientIP() returned whatever X-Forwarded-For said. The allowlist middleware compared that value with the operator's list.",
        "The report's runtime test shows the effect: a request from 1.2.3.4 with no header was refused, and the same request with X-Forwarded-For: 10.0.0.1 got status 200 with ClientIP 10.0.0.1 against an allowlist of 10.0.0.1. Because the access log and the transaction record took the same value, the attacker's real address appeared nowhere. It is rated medium (CVSS 4.0 5.3) under CWE-348, use of a less trusted source, affects 0.10.0 up to 1.6.0, and is fixed in 1.6.0, released July 19, 2026.",
        "Gin's own documentation warns that it trusts all proxies unless you name the trusted ones with SetTrustedProxies, calls that not safe, and says to pass nil when you don't use a proxy so ClientIP() returns the connecting address."
      ]
    },
    {
      "heading": "Which address you can trust",
      "points": [
        "MDN's X-Forwarded-For reference states the rule. Each proxy appends the address it received the request from, so only the entries added by proxies you control are trustworthy; anything to their left can be spoofed. Security uses such as rate limiting and IP-based access control must only use addresses added by a trusted proxy, and if the server can be reached directly from the internet, even when it also sits behind a trusted proxy, no part of the list can be trusted.",
        "To pick the address, MDN describes two methods: count your trusted proxies and read that many entries from the right, or skip from the right every address on your trusted-proxy list and use the first one that isn't. Combine multiple X-Forwarded-For headers into one list first. The address you get may belong to an untrusted intermediate proxy rather than the real client, but it is the only one fit for a security decision."
      ]
    },
    {
      "heading": "Check that a control is enforced, not only configured",
      "points": [
        "A configured setting isn't proof the request path checks it. In @sylphx/pdf-reader-mcp (GHSA-q344-5v34-gm84, CVE-2026-62264, high, CVSS 3.1 7.5, added to GitHub's advisory database October 29, 2026), HTTP mode read MCP_API_KEY and logged \"API key authentication enabled (X-API-Key header)\" at startup, but never validated the header. With HTTP mode bound to 0.0.0.0, any client that could reach the port could call every PDF tool and read any PDF the process could open. Version 3.0.1 checks the key with a constant-time comparison, answers 401 when it is missing or wrong, binds to 127.0.0.1 and warns when a non-loopback host has no key.",
        "Both advisories have the same shape: a control the operator could see in config and logs, and a request path that didn't depend on it. The only way to know is to send the request the control should stop, from where an attacker would send it, and watch it refused. Put those probes in CI so a refactor can't quietly remove the check."
      ]
    }
  ],
  "checks": [],
  "ask_rightshold": {
    "start": "https://www.rightshold.com/start?ref=seo",
    "mcp_endpoint": "https://www.rightshold.com/mcp",
    "ask": "We set trusted proxies on our MCP server and re-ran the outside probes: a non-allowlisted address with X-Forwarded-For set to an allowed IP, and a call with no API key, were both refused. Record the change and the probe results so we have a dated record outside our own logs.",
    "mcp_call": {
      "tool": "record_action",
      "arguments": {
        "action_type": "security.config_change",
        "outcome": "success",
        "occurred_at": "2026-11-13T23:30:00Z",
        "client_event_id": "mcp-allowlist:trusted-proxies-2026-11-13",
        "summary": "MCP endpoint: trusted proxies set to the load balancer's address only; client IP taken from the rightmost untrusted X-Forwarded-For entry; port reachable only from the load balancer. Probes from an outside address: spoofed X-Forwarded-For with an allowlisted IP refused 403; no API key refused 401",
        "action_sha256": "<sha256 of your probe script output>"
      }
    },
    "also": [
      "Needs your API key (or accept_terms:true once to open a free account; the key comes back once). It returns a ledger-committed receipt of what you submitted and when. It proves the entry was recorded, not that the probes ran or that the server is configured as described.",
      "It doesn't test your allowlist, read your proxy or framework settings, send spoofed headers or see client addresses. Run the probes yourself, and keep IP addresses and hostnames out of summary if they are sensitive; the hash ties the receipt to your full probe output."
    ],
    "scope": "Rightshold isn't affiliated with ezBookkeeping, Gin, Sylphx, MDN or the Model Context Protocol project, and it doesn't scan servers, test allowlists or inspect proxy settings. Advisory details are as published in GitHub's advisory database and the mayswind/ezbookkeeping and SylphxAI/pdf-reader-mcp repositories, the 1.6.0 change as listed in ezBookkeeping's release notes, Gin's proxy guidance as written in its docs/doc.md, and the header rules from MDN's X-Forwarded-For reference, all read November 13, 2026; advisories get updated, so confirm fixed versions on the linked pages."
  },
  "sources": [
    {
      "title": "mayswind/ezbookkeeping: GHSA-wchh-mrv2-5h4q, source-IP allowlist (MCP and API token) bypassable via a spoofed X-Forwarded-For header (CVE-2026-86041)",
      "url": "https://github.com/mayswind/ezbookkeeping/security/advisories/GHSA-wchh-mrv2-5h4q"
    },
    {
      "title": "GitHub Advisory Database: GHSA-wchh-mrv2-5h4q",
      "url": "https://github.com/advisories/GHSA-wchh-mrv2-5h4q"
    },
    {
      "title": "mayswind/ezbookkeeping: v1.6.0 release notes",
      "url": "https://github.com/mayswind/ezbookkeeping/releases/tag/v1.6.0"
    },
    {
      "title": "Gin documentation: Don't trust all proxies",
      "url": "https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies"
    },
    {
      "title": "MDN Web Docs: X-Forwarded-For (security and privacy concerns, selecting an IP address)",
      "url": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-For"
    },
    {
      "title": "SylphxAI/pdf-reader-mcp: GHSA-q344-5v34-gm84, HTTP transport accepts unauthenticated requests despite a configured MCP_API_KEY (CVE-2026-62264)",
      "url": "https://github.com/SylphxAI/pdf-reader-mcp/security/advisories/GHSA-q344-5v34-gm84"
    }
  ],
  "related": [
    {
      "title": "Is it safe to run an MCP server in HTTP mode without authentication?",
      "url": "https://www.rightshold.com/answers/is-it-safe-to-run-an-mcp-server-in-http-mode-without-authentication"
    },
    {
      "title": "Keep MCP as a thin adapter and recheck authorization on every tool call",
      "url": "https://www.rightshold.com/answers/how-to-keep-mcp-as-a-thin-adapter-and-recheck-authorization-on-every-tool-call"
    },
    {
      "title": "Record MCP tool calls at the gateway with scope-grant binding",
      "url": "https://www.rightshold.com/answers/how-to-record-mcp-tool-calls-at-the-gateway-with-scope-grant-binding"
    },
    {
      "title": "Rate limiting an AI agent's API calls per key",
      "url": "https://www.rightshold.com/answers/how-to-rate-limit-an-ai-agents-api-calls-per-key"
    },
    {
      "title": "How do you keep one client from crashing or locking up an MCP server's HTTP mode?",
      "url": "https://www.rightshold.com/answers/how-to-keep-one-client-from-crashing-or-locking-up-an-mcp-servers-http-mode"
    }
  ],
  "computed_from": "Curated guidance; license verdicts computed from Rightshold's license profiles at render time. Not legal advice.",
  "updated": "2026-11-13"
}