Skip to content

fix(ip): resolve client IP from a trusted proxy hop, not X-Forwarded-For[0]#7

Open
dddabtc wants to merge 1 commit into
leyten:masterfrom
dddabtc:fix/client-ip-xff-spoofing
Open

fix(ip): resolve client IP from a trusted proxy hop, not X-Forwarded-For[0]#7
dddabtc wants to merge 1 commit into
leyten:masterfrom
dddabtc:fix/client-ip-xff-spoofing

Conversation

@dddabtc

@dddabtc dddabtc commented Jun 20, 2026

Copy link
Copy Markdown

Problem

/api/anon and /api/auth/callback both compute the client IP as:

function clientIp(req) {
  const xff = req.headers.get('x-forwarded-for');
  if (xff) return xff.split(',')[0].trim();   // left-most = most client-controllable
  return req.headers.get('x-real-ip') || '0.0.0.0';
}

X-Forwarded-For is a client-settable header and the left-most entry is the one a caller can forge. The IP is used for per-IP caps:

  • the anonymous free-prompt allowance (hashIp is bound into the anon token; rotating the header mints fresh free quota and can drain the daily free-subsidy budget),
  • the per-IP account-creation limit (recordNewAccountForIp), an anti-bot control.

So an attacker rotates X-Forwarded-For to bypass both.

Fix

Add a shared lib/client-ip.ts that returns the entry inserted by the outermost trusted proxy — the XFF_TRUSTED_PROXY_COUNT-th from the right (default 1, i.e. a single CDN/load balancer) — rather than the attacker-controllable left-most value. Both routes use it.

Operators set XFF_TRUSTED_PROXY_COUNT to their real proxy hop count. (If the app is exposed with no proxy at all, X-Forwarded-For can't be trusted regardless — a proxy must set it.)

…For[0]

Both /api/anon and /api/auth/callback derived the client IP as
xff.split(',')[0] — the left-most X-Forwarded-For entry, which is the most
client-controllable. A caller could prepend any value to rotate their apparent
IP and bypass per-IP limits: the anonymous free-prompt cap (drain the daily
free-subsidy budget) and the per-IP account-creation cap (defeat the anti-bot
limit).

Add a shared clientIp() that reads the entry inserted by the outermost trusted
proxy (XFF_TRUSTED_PROXY_COUNT from the right, default 1) instead of the
attacker-controllable left-most one, and use it in both routes.
leyten added a commit that referenced this pull request Jul 21, 2026
…itors (P1-#4 #7) (#47)

GradedReputation passed as trust (rejects proven cheaters at announce);
auditors from SWARM_AUDITOR_PUBKEYS; swarmSpotCheckSweep probabilistically
checks ready rings; reputation persisted to data/ so a cheater cant rejoin
by waiting for a deploy. reputation-gate-test 9/9; all suites green.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants