{"id":"CVE-2026-86041","aliases":["EUVD-2026-117162","GHSA-wchh-mrv2-5h4q"],"status":"published","title":"ezBookkeeping: Source-IP allowlist (MCP \u0026 API token) is bypassable via a spoofed `X-Forwarded-For` header","description":"ezBookkeeping is an open source, self-hosted personal finance application. From 0.10.0 until 1.6.0, ezBookkeeping constructs its Gin router with gin.New() without configuring SetTrustedProxies, causing c.ClientIP() to trust a spoofed X-Forwarded-For value. An unauthenticated remote attacker can supply an allowlisted address to bypass the mcp_allowed_remote_ips or api_token_allowed_remote_ips gate. A valid MCP or API token remains required for protected actions. When such actions occur, the same spoofed value is stored in Transaction.CreatedIp and the request log, attributing activity to the trusted host instead of the attacker. This issue is fixed in version 1.6.0.","severity":{"version":"4.0","score":5.3,"vector":"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","level":"medium","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-11","score":0.00221,"percentile":0.12793,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-348"],"published":"2026-11-04T22:15:56.477Z","modified":"2026-11-09T22:00:06Z","fixes":[{"ecosystem":"Go","package":"github.com/mayswind/ezbookkeeping","fixed_in":["1.6.0"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"Go","package":"github.com/mayswind/ezbookkeeping","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0.10.0"},{"fixed":"1.6.0"}]}]},{"source":"osv","ecosystem":"Go","package":"github.com/mayswind/ezbookkeeping","ranges":[{"type":"SEMVER","events":[{"introduced":"0.10.0"},{"fixed":"1.6.0"}]}]}],"products":[{"vendor":"mayswind","product":"ezbookkeeping"}],"references":[{"url":"https://github.com/mayswind/ezbookkeeping/security/advisories/GHSA-wchh-mrv2-5h4q","tags":["x_refsource_CONFIRM","exploit","advisory","web"]},{"url":"https://github.com/mayswind/ezbookkeeping/commit/d79a5beb8037e529dc4dc64a6d22bd2d02b2e986","tags":["x_refsource_MISC","fix","web"]},{"url":"https://github.com/mayswind/ezbookkeeping/releases/tag/v1.6.0","tags":["x_refsource_MISC","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-86041","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-wchh-mrv2-5h4q"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/86xxx/CVE-2026-86041.json","tags":["advisory"]},{"url":"https://github.com/mayswind/ezbookkeeping","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-86041","title":"ezBookkeeping: Source-IP allowlist (MCP \u0026 API token) is bypassable via a spoofed `X-Forwarded-For` header","description":"ezBookkeeping is an open source, self-hosted personal finance application. From 0.10.0 until 1.6.0, ezBookkeeping constructs its Gin router with gin.New() without configuring SetTrustedProxies, causing c.ClientIP() to trust a spoofed X-Forwarded-For value. An unauthenticated remote attacker can supply an allowlisted address to bypass the mcp_allowed_remote_ips or api_token_allowed_remote_ips gate. A valid MCP or API token remains required for protected actions. When such actions occur, the same spoofed value is stored in Transaction.CreatedIp and the request log, attributing activity to the trusted host instead of the attacker. This issue is fixed in version 1.6.0.","status":"published","published":"2026-11-04T22:15:56.477Z","modified":"2026-11-05T19:48:52.33Z","cvss":[{"version":"4.0","score":5.3,"vector":"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","level":"medium","assessor":"cna"}],"cwes":["CWE-348"],"updated_at":"2026-11-05T20:54:59.980292Z"}],"euvd":[{"id":"EUVD-2026-117162","description":"ezBookkeeping: Source-IP allowlist (MCP \u0026 API token) is bypassable via a spoofed `X-Forwarded-For` header","status":"published","published":"2026-11-09T21:51:28Z","modified":"2026-11-09T21:51:28Z","cvss":[{"version":"4.0","score":5.3,"vector":"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","level":"medium"}],"updated_at":"2026-11-09T23:18:32.235826Z"}],"ghsa":[{"id":"GHSA-wchh-mrv2-5h4q","title":"ezBookkeeping: Source-IP allowlist (MCP \u0026 API token) is bypassable via a spoofed `X-Forwarded-For` header","description":"### 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.)","status":"published","published":"2026-11-09T21:51:28Z","modified":"2026-11-09T21:52:25Z","cvss":[{"version":"4.0","score":5.3,"vector":"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","level":"medium"}],"cwes":["CWE-348"],"updated_at":"2026-11-09T22:20:10.289641Z"}],"nvd":[{"id":"CVE-2026-86041","description":"ezBookkeeping is an open source, self-hosted personal finance application. From 0.10.0 until 1.6.0, ezBookkeeping constructs its Gin router with gin.New() without configuring SetTrustedProxies, causing c.ClientIP() to trust a spoofed X-Forwarded-For value. An unauthenticated remote attacker can supply an allowlisted address to bypass the mcp_allowed_remote_ips or api_token_allowed_remote_ips gate. A valid MCP or API token remains required for protected actions. When such actions occur, the same spoofed value is stored in Transaction.CreatedIp and the request log, attributing activity to the trusted host instead of the attacker. This issue is fixed in version 1.6.0.","status":"published","published":"2026-11-04T23:17:58.903Z","modified":"2026-11-05T20:18:00.203Z","cvss":[{"version":"4.0","score":5.3,"vector":"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/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","level":"medium","assessor":"secondary"}],"cwes":["CWE-348"],"updated_at":"2026-11-05T21:56:49.918687Z"}],"osv":[{"id":"CVE-2026-86041","title":"ezBookkeeping: Source-IP allowlist (MCP \u0026 API token) is bypassable via a spoofed `X-Forwarded-For` header","description":"ezBookkeeping is an open source, self-hosted personal finance application. From 0.10.0 until 1.6.0, ezBookkeeping constructs its Gin router with gin.New() without configuring SetTrustedProxies, causing c.ClientIP() to trust a spoofed X-Forwarded-For value. An unauthenticated remote attacker can supply an allowlisted address to bypass the mcp_allowed_remote_ips or api_token_allowed_remote_ips gate. A valid MCP or API token remains required for protected actions. When such actions occur, the same spoofed value is stored in Transaction.CreatedIp and the request log, attributing activity to the trusted host instead of the attacker. This issue is fixed in version 1.6.0.","status":"published","published":"2026-11-04T22:15:56.477Z","modified":"2026-11-07T02:31:06.684334678Z","cvss":[{"version":"4.0","score":5.3,"vector":"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","level":"medium"}],"cwes":["CWE-348"],"updated_at":"2026-11-07T03:08:11.332776Z"},{"id":"GHSA-wchh-mrv2-5h4q","title":"ezBookkeeping: Source-IP allowlist (MCP \u0026 API token) is bypassable via a spoofed `X-Forwarded-For` header","description":"### 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.)","status":"published","published":"2026-11-09T21:51:28Z","modified":"2026-11-09T22:00:06.734181917Z","cvss":[{"version":"4.0","score":5.3,"vector":"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","level":"medium"}],"cwes":["CWE-348"],"updated_at":"2026-11-09T22:10:51.322704Z"}]}}
