{"id":"GHSA-wchh-mrv2-5h4q","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: \u003callowlisted-ip\u003e` 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 -\u003e status=400 (correctly blocked)\n[exploit] XFF=10.0.0.1, RemoteAddr=1.2.3.4 -\u003e 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.)","aliases":["CVE-2026-86041"],"modified":"2026-11-09T22:00:06.734181917Z","published":"2026-11-09T21:51:28Z","database_specific":{"github_reviewed":true,"github_reviewed_at":"2026-11-09T21:51:28Z","nvd_published_at":"2026-11-04T23:17:58Z","cwe_ids":["CWE-348"],"severity":"MODERATE"},"references":[{"type":"WEB","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":"WEB","url":"https://github.com/mayswind/ezbookkeeping/commit/d79a5beb8037e529dc4dc64a6d22bd2d02b2e986"},{"type":"PACKAGE","url":"https://github.com/mayswind/ezbookkeeping"},{"type":"WEB","url":"https://github.com/mayswind/ezbookkeeping/releases/tag/v1.6.0"}],"affected":[{"package":{"name":"github.com/mayswind/ezbookkeeping","ecosystem":"Go","purl":"pkg:golang/github.com/mayswind/ezbookkeeping"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0.10.0"},{"fixed":"1.6.0"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/11/GHSA-wchh-mrv2-5h4q/GHSA-wchh-mrv2-5h4q.json"}}],"schema_version":"1.9.2","severity":[{"type":"CVSS_V4","score":"CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N"}]}