fix(security): stop trusting forged client IPs from the whole private range - #127
Open
fjaeckel wants to merge 1 commit into
Open
fix(security): stop trusting forged client IPs from the whole private range#127fjaeckel wants to merge 1 commit into
fjaeckel wants to merge 1 commit into
Conversation
… range
c.ClientIP() keys every IP-based rate limit and the client_ip recorded in the
access log. When the connecting peer is a trusted proxy, Gin takes that value
from a client-supplied X-Real-IP / X-Forwarded-For header. The trusted set was
all of RFC-1918 plus loopback, and the compose file published the API port --
so traffic reaching the API directly arrived from the Docker bridge, inside
172.16.0.0/12, and the header was honoured verbatim.
Verified against a running instance with rate limiting enabled: after the real
source was throttled (10 x 429), 40 logins with a rotating X-Real-IP returned
0 x 429, and 30 registrations with rotating IPs all returned 201. A request
carrying 'X-Real-IP: 203.0.113.77' was logged with that value as client_ip, so
an attacker can also write arbitrary source addresses into the access log.
The per-account lockout still capped single-account password guessing and is
unaffected; what the IP limits alone guarded -- password spraying, reset and
verification email flooding, registration flooding, mass account-lockout DoS --
was bypassable.
Changes:
- Trusted proxies are configurable via TRUSTED_PROXIES and now default to
loopback only. A startup warning names the setting when it is unset.
- docker-compose binds the API port to 127.0.0.1 (override with API_BIND),
so the API is reached through the nginx ingress rather than directly. This
also stops /metrics being reachable off-host.
- TRUSTED_PROXIES documented in docker-compose.yml and .env.example.
Deployments behind a reverse proxy MUST set TRUSTED_PROXIES to the ingress
address, otherwise forwarded client IPs are ignored and all proxied traffic
shares one rate-limit bucket.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GukWfyJMY28qv2CJjxFvKF
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes a Medium severity rate-limit bypass and access-log forgery.
c.ClientIP()is the key for every IP-based rate limit and theclient_ipwritten to the access log. When the connecting peer is a trusted proxy, Gin takes that value from a client-suppliedX-Real-IP/X-Forwarded-Forheader.The trusted set was all of RFC-1918 plus loopback:
…and
docker-compose.ymlpublished the API port. So traffic reaching the API directly arrived from the Docker bridge — inside172.16.0.0/12— and the header was honoured verbatim.Verified against a running instance
With rate limiting enabled:
POST /auth/login, no headerX-Real-IP, from the already-throttled sourceX-Forwarded-ForPOST /auth/registerwith rotating IPsX-Real-IP: 203.0.113.77"client_ip":"203.0.113.77"— log forgeryScope, honestly
The per-account lockout still held — 5 wrong logins lock an account even across rotating spoofed IPs, so single-account password guessing was never helped by this. What the IP limits alone guarded was bypassable: password spraying, password-reset/verification email flooding, registration flooding, and mass account-lockout DoS against arbitrary victims.
Behind an nginx-only ingress the app was already safe, since nginx's
proxy_set_header X-Real-IP $remote_addroverwrites any client value.Changes
TRUSTED_PROXIES, defaulting to loopback only. A startup warning names the setting when unset.docker-composebinds the API port to127.0.0.1(override withAPI_BIND), so the API is reached through the nginx ingress rather than directly. This also stops/metricsbeing reachable off-host.TRUSTED_PROXIESdocumented indocker-compose.ymland.env.example; compose defaults it to172.16.0.0/12so the bundled nginx keeps working out of the box.Deployments behind a reverse proxy must set
TRUSTED_PROXIESto the ingress address. If it's unset, forwarded client IPs are ignored and all proxied traffic shares a single rate-limit bucket (fails closed, but coarsely). Narrow it to the proxy's actual address where you can pin it.Tests
splitAndTrimparsing, and an assertion that the default trusted set contains no broad private range — so the insecure default can't come back silently.Full suite: 24 packages passing,
gofmtclean.🤖 Generated with Claude Code
Generated by Claude Code