CVE-2026-86041
ezBookkeeping: Source-IP allowlist (MCP & API token) is bypassable via a spoofed `X-Forwarded-For` header
CVE-2026-86041 is a medium vulnerability (CVSS 5.3) in Mayswind Ezbookkeeping, published November 4, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.22%, the 12th percentile. Fixed in github.com/mayswind/ezbookkeeping 1.6.0.
- Severity
- 5.3 MediumCVSS 4.0, CVE List
- Exploit chance
- 0.22%EPSS, 12th percentile
- Exploitation
- None knownNot on CISA’s or ENISA’s lists
- Fixed in
1.6.0github.com/mayswind/ezbookkeeping
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.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- Low
- Privileges needed
- Low
- User interaction
- None
What an attacker can affect
- Confidentiality
- None
- Integrity
- Low
- Availability
- None
Exploit likelihood
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
github.com/mayswind/ezbookkeepingGo | 0.10.0 up to 1.6.0 | 1.6.0 |
What each source reports
CVE List Score used 5.3
ezBookkeeping: Source-IP allowlist (MCP & API token) is bypassable via a spoofed `X-Forwarded-For` header
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.
- Record
CVE-2026-86041- CVSS 4.0
- 5.3 Medium
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 - Weakness
- CWE-348
- Updated
- 2026-11-05
EUVDEUVD-2026-117162 5.3
ezBookkeeping: Source-IP allowlist (MCP & API token) is bypassable via a spoofed X-Forwarded-For header
- Record
EUVD-2026-117162- CVSS 4.0
- 5.3 Medium
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 - Updated
- 2026-11-09
GitHub AdvisoriesGHSA-wchh-mrv2-5h4q 5.3
ezBookkeeping: Source-IP allowlist (MCP & API token) is bypassable via a spoofed `X-Forwarded-For` header
Summary
A 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.
Details
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().
Both allowlist middlewares match the operator allowlist against this attacker-controlled value:
// pkg/middlewares/mcp_server_ip_limit.go:19
if config.MCPAllowedRemoteIPs[i].Match(c.ClientIP()) { c.Next(); return }
The 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.
PoC
Runtime PoC (no live deployment required) — pkg/middlewares/xff_bypass_runtime_test.go, pkg/middlewares/xff_chain_runtime_test.go:
go test ./pkg/middlewares/ -run 'TestACCESSCONTROL001_XFFBypass|TestCHAIN_AllowlistBypass' -v
Output:
[control] no XFF, RemoteAddr=1.2.3.4 -> status=400 (correctly blocked)
[exploit] XFF=10.0.0.1, RemoteAddr=1.2.3.4 -> status=200, ClientIP=10.0.0.1 (bypassed)
gate status = 200 | created_ip that would be persisted = "10.0.0.1"
Against a live instance with mcp_allowed_remote_ips=10.0.0.1:
curl -s -o /dev/null -w "%{http_code}\n" https://target/mcp # 403 (blocked)
curl -s -o /dev/null -w "%{http_code}\n" -H "X-Forwarded-For: 10.0.0.1" https://target/mcp # passes IP gate
Impact
Defeats 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.)
- Record
GHSA-wchh-mrv2-5h4q- CVSS 4.0
- 5.3 Medium
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 - Weakness
- CWE-348
- Updated
- 2026-11-09
NVD 5.3
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.
- Record
CVE-2026-86041- CVSS 4.0
- 5.3 Medium
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 - Weakness
- CWE-348
- Updated
- 2026-11-12
OSV 5.3
ezBookkeeping: Source-IP allowlist (MCP & API token) is bypassable via a spoofed `X-Forwarded-For` header
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.
- Record
CVE-2026-86041- CVSS 4.0
- 5.3 Medium
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 - Weakness
- CWE-348
- Updated
- 2026-11-07
OSVGHSA-wchh-mrv2-5h4q 5.3
ezBookkeeping: Source-IP allowlist (MCP & API token) is bypassable via a spoofed `X-Forwarded-For` header
Summary
A 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.
Details
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().
Both allowlist middlewares match the operator allowlist against this attacker-controlled value:
// pkg/middlewares/mcp_server_ip_limit.go:19
if config.MCPAllowedRemoteIPs[i].Match(c.ClientIP()) { c.Next(); return }
The 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.
PoC
Runtime PoC (no live deployment required) — pkg/middlewares/xff_bypass_runtime_test.go, pkg/middlewares/xff_chain_runtime_test.go:
go test ./pkg/middlewares/ -run 'TestACCESSCONTROL001_XFFBypass|TestCHAIN_AllowlistBypass' -v
Output:
[control] no XFF, RemoteAddr=1.2.3.4 -> status=400 (correctly blocked)
[exploit] XFF=10.0.0.1, RemoteAddr=1.2.3.4 -> status=200, ClientIP=10.0.0.1 (bypassed)
gate status = 200 | created_ip that would be persisted = "10.0.0.1"
Against a live instance with mcp_allowed_remote_ips=10.0.0.1:
curl -s -o /dev/null -w "%{http_code}\n" https://target/mcp # 403 (blocked)
curl -s -o /dev/null -w "%{http_code}\n" -H "X-Forwarded-For: 10.0.0.1" https://target/mcp # passes IP gate
Impact
Defeats 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.)
- Record
GHSA-wchh-mrv2-5h4q- CVSS 4.0
- 5.3 Medium
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 - Weakness
- CWE-348
- Updated
- 2026-11-09
History
| Recorded | Changed | Sources |
|---|---|---|
| published | EUVD | |
| affected, references | GitHub Advisories | |
| affected, facts, references | EUVD, NVD, OSV | |
| references | CVE List | |
| aliases, references | NVD, OSV | |
| aliases, published | EUVD | |
| first indexed | CVE List |
Dates are when Every CVE recorded each change. Upstream dates may differ.
References
github.com/mayswind/ezbookkeeping/security/advisories/GHSA-wchh-mrv2-5h4q
github.com/mayswind/ezbookkeeping/commit/d79a5beb8037e529dc4dc64a6d22bd2d02b2e986
github.com/mayswind/ezbookkeeping/releases/tag/v1.6.0
nvd.nist.gov/vuln/detail/CVE-2026-86041
github.com/advisories/GHSA-wchh-mrv2-5h4q
github.com/CVEProject/cvelistV5/tree/main/cves/2026/86xxx/CVE-2026-86041.json
github.com/mayswind/ezbookkeeping