{"id":"CVE-2026-86041","aliases":["GHSA-wchh-mrv2-5h4q"],"url":"https://o3.security/vulnerability/CVE-2026-86041","summary":"ezBookkeeping: Source-IP allowlist (MCP & API token) is bypassable via a spoofed `X-Forwarded-For` header","details":"### Summary\nA remote attacker from any IP address defeats the operator-configured source-IP allowlist (`mcp_allowed_remote_ips` / `api_token_allowed_remote_ips`) by sending a single `X-Forwarded-For: <allowlisted-ip>` header. The router is created with `gin.New()` and never calls `SetTrustedProxies`, so gin trusts every proxy by default and `c.ClientIP()` returns the attacker-supplied header verbatim. The same forged value is also written to `Transaction.CreatedIp` and the access log, so the attacker's real IP never appears in any record.\n\n### Details\n`cmd/webserver.go:104` builds the router with `gin.New()`. A grep of `cmd/` and `pkg/` confirms `SetTrustedProxies` is never called, leaving gin's default trusted-proxy range of `0.0.0.0/0` and `::/0`. `WebContext` (`pkg/core/context_web.go:39`) embeds `*gin.Context` and does not override `ClientIP()`.\n\nBoth allowlist middlewares match the operator allowlist against this attacker-controlled value:\n\n```go\n// pkg/middlewares/mcp_server_ip_limit.go:19\nif config.MCPAllowedRemoteIPs[i].Match(c.ClientIP()) { c.Next(); return }\n```\n\nThe identical `c.ClientIP()` is consumed by the audit trail at `pkg/api/transactions.go:1078` (`createNewTransactionModel(..., c.ClientIP())` → `CreatedIp`) and the access log (`pkg/middlewares/request_log.go:28`), so one header both bypasses the gate and forges attribution.\n\n### PoC\nRuntime PoC (no live deployment required) — `pkg/middlewares/xff_bypass_runtime_test.go`, `pkg/middlewares/xff_chain_runtime_test.go`:\n\n```\ngo test ./pkg/middlewares/ -run 'TestACCESSCONTROL001_XFFBypass|TestCHAIN_AllowlistBypass' -v\n```\n\nOutput:\n```\n[control] no XFF, RemoteAddr=1.2.3.4 -> status=400 (correctly blocked)\n[exploit] XFF=10.0.0.1, RemoteAddr=1.2.3.4 -> status=200, ClientIP=10.0.0.1 (bypassed)\ngate status = 200 | created_ip that would be persisted = \"10.0.0.1\"\n```\n\nAgainst a live instance with `mcp_allowed_remote_ips=10.0.0.1`:\n```\ncurl -s -o /dev/null -w \"%{http_code}\\n\" https://target/mcp            # 403 (blocked)\ncurl -s -o /dev/null -w \"%{http_code}\\n\" -H \"X-Forwarded-For: 10.0.0.1\" https://target/mcp  # passes IP gate\n```\n\n### Impact\nDefeats an explicitly advertised, operator-configured security control on the MCP and API-token surfaces, from any source IP, with a single header and no authentication to the gate itself. It also destroys non-repudiation: every action is logged under the spoofed allowlisted IP, so audit trails point at the trusted host instead of the attacker. (The MCP/API actions themselves still require a valid token — this neutralizes the compensating IP control, not the JWT layer.)","published":"2026-11-04T22:15:56.477Z","modified":"2026-11-07T02:31:06.684334678Z","cvss":null,"epss":null,"cisaKev":null,"exploitsKnown":null,"affectedPackages":[{"ecosystem":"Go","name":"github.com/mayswind/ezbookkeeping","fixedVersion":"1.6.0"}],"fix":{"url":"https://github.com/mayswind/ezbookkeeping/commit/d79a5beb8037e529dc4dc64a6d22bd2d02b2e986","label":"mayswind/ezbookkeeping@d79a5be"},"references":[{"type":"WEB","url":"https://github.com/mayswind/ezbookkeeping/releases/tag/v1.6.0"},{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/86xxx/CVE-2026-86041.json"},{"type":"ADVISORY","url":"https://github.com/mayswind/ezbookkeeping/security/advisories/GHSA-wchh-mrv2-5h4q"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-86041"},{"type":"FIX","url":"https://github.com/mayswind/ezbookkeeping/commit/d79a5beb8037e529dc4dc64a6d22bd2d02b2e986"},{"type":"PACKAGE","url":"https://github.com/mayswind/ezbookkeeping"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-11-07T02:31:06.684334678Z"}}