Stop believing an address a client made up (GRYT-179) - #45
Merged
Conversation
getClientIp read x-forwarded-for and took the first value, falling back to the socket address only when the header was absent. That header is one any client can set, and the leftmost entry is precisely the part a client controls — so every per-IP protection in the server could be walked around by varying one string per connection. Not theory. Measured 30 joins against a cap of 19 while testing something else, and the same trick defeats the invite brute-force cooldown, which is the thing standing between an attacker and guessing codes. Now it counts from the right, which is the only end that means anything: x-forwarded-for grows left to right as a request crosses proxies, so the rightmost entries were appended by infrastructure and the leftmost is whatever was claimed. Asking for more hops than the chain holds means the request did not come through them, so the socket address wins rather than the shortfall. Trusts nothing by default. That is the safe end of the trade but it is a trade: a server behind a reverse proxy or tunnel must set GRYT_TRUSTED_PROXY_HOPS=1, or every client arrives wearing the proxy's address and shares one rate-limit bucket with everyone else. Said plainly in example.env, in capitals, because getting it wrong is quiet in one direction and loud in the other. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
getClientIpreadx-forwarded-forand took the first value, falling back to the socket address only when the header was absent. That header is one any client can set, and the leftmost entry is precisely the part a client controls.So every per-IP protection in the server could be walked around by varying one string per connection.
Measured, not theorised: 30 joins against a cap of 19, while testing server#44. The same trick defeats the invite brute-force cooldown — the thing standing between an attacker and guessing invite codes — and the per-IP half of every chat and moderation limit.
The fix
Count from the right, which is the only end that means anything.
x-forwarded-forgrows left to right as a request crosses proxies, so the rightmost entries were appended by infrastructure and the leftmost is whatever was claimed. Asking for more hops than the chain holds means the request didn't come through them, so the socket address wins rather than the shortfall.Read this bit before merging
Nothing is trusted by default, and that is a trade rather than a free win.
Direct exposure: correct and now unspoofable.
Behind a reverse proxy or tunnel — which is how
dev.lanruns — you must setGRYT_TRUSTED_PROXY_HOPS=1. Otherwise every client arrives wearing the proxy's address and shares a single rate-limit bucket, so one noisy person throttles everybody. That failure is quiet in one direction and loud in the other, which is why it's inexample.envin capitals.Worth checking what the tunnel in front of
dev.lanactually appends before picking the number.Verified against a live server
x-forwarded-for, default configrate_limited— same as an honest client, spoofing buys nothingGRYT_TRUSTED_PROXY_HOPS=1, attacker varies the left value, real address on the rightrate_limited— the claim is ignored, the appended address is usedThat last row is the one that shows it reads the correct end.
Also
Documented
GRYT_INVITE_MAX_JOINS_PER_HOURfrom server#44 inexample.envwhile I was in there — it was added without an entry.🤖 Generated with Claude Code