Gate forwarded-header trust behind trusted-proxy config in rate limiters #88
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Tracking issue from the PR #87 review (advisory, non-blocking there because it matches the agreed plan and the existing
LoginRateLimitpattern).Problem
Both
ReceiverRateLimitandLoginRateLimit(internal/middleware/ratelimit.go) key onhttprate.KeyByRealIP, which trustsTrue-Client-IP,X-Real-IP, and the firstX-Forwarded-Forentry unconditionally. The first XFF entry is client-controlled even behind an appending reverse proxy, so a deliberate attacker can:REPO_POLICIES.mdrequires trusted-proxy-gated forwarded-header handling before 1.0.Definition of done
RemoteAddr) is within a configured trusted-proxy set (env-configured CIDR list; set-but-unparseable value aborts startup, per repo policy). Otherwise the key isRemoteAddr.Minor cleanups to fold in (same files, from the same review)
internal/config/config_test.go: theRECEIVER_RATE_LIMITerror-path tests assert onlyassert.Error; tighten toErrorContains(variable name in the message) /errors.Is(config.ErrNonPositiveValue).internal/middleware/ratelimit.go: the two 429 limit-handler bodies are near-duplicates; extract a shared helper.Scope note: the PR #83 review found the same unconditional forwarded-header trust in the new
PasswordChangeRateLimit(added in commit8362ce9, shares thepostRateLimithelper withLoginRateLimit). This issue covers ALL rate limiters — receiver, login, and password change — via the one shared key function described in the DoD.clawbot referenced this issue2026-08-11 14:48:08 +02:00
Implemented in #122.
All three limiters (receiver, login, password change) now share one key function: the connection's own address, unless the direct peer is inside a network listed in the new
TRUSTED_PROXIESCIDR list, in which case the forwarded client address is used. The list is empty by default, so nothing is trusted until an operator names their proxy; a set-but-unparseable value aborts startup (config.ErrInvalidCIDR), matching the other parsed variables. Within a trusted requestX-Forwarded-Foris walked right to left and the first hop that is not itself a trusted proxy wins, so client-prepended entries cannot be selected. Documented in the README.Both folded-in cleanups are in the same commit: the 429 responder is extracted into one helper, and the
RECEIVER_RATE_LIMITerror-path tests now assert the failure names the variable and, for zero/negative, wrapsErrNonPositiveValue.Verified: the bypass test bites — reverting the key function to plain
httprate.KeyByRealIPmakesTestRateLimitKey_SpoofedForwardedFromUntrustedPeerfail (expected 429, actual 200) for all three header names, along with the chain-walk and receiver variants; restoring it makes them pass.make checkexit 0, anddocker build --no-cache-filter=lint,builderranmake lint(0 issues) andmake testin-container with no cached package results.