- Shipped
- September 23, 2026 at 10:21 AM UTC
- Author
- Kamo
- Commit
- 65f3228
forward() set the outgoing X-Real-IP to X-Forwarded-For.split(",")[0] -- the LEFT-most hop, which is the one part of that header a client controls outright: a request carrying "X-Forwarded-For: 8.8.8.8" arrives at Traefik, which appends the address it actually saw, and the header becomes "8.8.8.8, <real client>". SecurityService's EconsentController stores this gateway's X-Real-IP verbatim on a legal e-consent record, so any visitor could attest to whatever address they liked just by sending their own X-Forwarded-For. Added ClientIpResolver (com.kamo.api.app.config), which walks the list from the right and takes the first PUBLIC hop -- everything to the right of the real client is our own infrastructure, and an in-cluster hop is always private and skipped. This is the same algorithm AccessEnforcementFilter already had, private to itself, for judging access rules by IP; that filter now delegates to the extracted class instead of keeping its own copy, so forward() and the filter agree by construction rather than by two authors independently getting the same nine lines right. X-Forwarded-For itself is unchanged -- still forwarded as received (or synthesised from the socket peer when absent). The same right-most-public-hop rule is duplicated per-service across the platform (SecurityService's ClientIpResolver, ESigService's EsignClientIp, DocsService's LegalClientIp) because none of those services is importable from another; this is apiservice's copy. XRealIpForwardingTest drives a real forwarded request carrying a spoofed left hop in front of a genuine one and asserts the upstream request's X-Real-IP is the genuine address, plus that an internal hop appended after the real client is skipped. ClientIpResolverTest covers the resolver directly. Reverting to the left-most split fails XRealIpForwardingTest immediately.
