Cap the X-Forwarded-For hop walk at 64 entries (closes #124) #129
Reference in New Issue
Block a user
Delete Branch "issue-124-cap-xff-hops"
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?
Closes #124.
What changed
internal/middleware/ratelimit.go:maxForwardedHops = 64constant.forwardedClientAddrnow examines only the last 64 entries of the chain; longer chains are truncated from the left before the walk. Running out of hops returns(Addr{}, false), soclientKeyfalls back to the peer address — the same fail-closed direction an unreadable hop already took. Bucket assignment for real chains (1-3 hops) is unchanged.RemoteAddrfallback. It claimed keying on the raw value avoids collapsing those peers into one bucket; on a Unix-socket listener every peer carries the sameRemoteAddrand does share one bucket. Behaviour unchanged, comment only.internal/middleware/ratelimit_test.go:TestRateLimitKey_LongChainCapsWalkAndFallsBackToPeerdrives the login limiter from a trusted peer with a 50k-hop chain (~0.9 MB) whose head entry rotates per request and whose remaining hops are all inside the trusted-proxy prefix. Every request must land in the peer's bucket, so the last one is rejected; it also asserts the run completes promptly.No change to IPv6 bucketing (#125 is a separate, undecided question on the same function).
TODO.mduntouched.Mutation evidence
With the truncation removed and everything else identical,
make test:The uncapped walk reaches the client-controlled head of the chain, so each rotated value mints a fresh bucket and nothing is rejected. With the cap restored the same test passes in 0.06s — 3.7x faster on the same input.
Gate evidence
make checkexit 0, lint0 issues.Containerized path with the cache defeated,
docker build --no-cache-filter=lint,builder --progress=plain ., exit 0. Both stages executed rather than replaying a cached log:The only
CACHEDlayers were dependency-fetch steps ahead of those twoRUNs.PASS — no findings. Independently mutation-checked in a fresh clone: deleting only the truncation fails
TestRateLimitKey_LongChainCapsWalkAndFallsBackToPeeratinternal/middleware/ratelimit_test.go:405with the stated message, and reversing it tohops[:maxForwardedHops]fails it too, so the test pins the direction as well as the cap; truncating the left end can only degrade a result to the peer fallback, never select a different address.Gate on
a60b96d:make checkexit 0;docker build --no-cache-filter=lint,builderexit 0 with#21 [lint 8/8] RUN make lint->0 issues.(DONE 71.9s) and#33 [builder 8/10] RUN make test->ok sneak.berlin/go/webhooker/internal/middleware 1.103s(DONE 68.7s), zero(cached)markers and noCACHEDon either RUN. The new test ran at 0.04s against its 2s bound, so that assertion is not flaky-by-construction.Disclosure: the Gitea check on this commit is still
pending/ "Waiting to run" (run 141) and the Actions API returns 403 for me, so the containerized run above is the only executed evidence I could obtain.