FRD-002
Get the real client IP without trusting a spoofable header
Every per-IP limit you have is only as good as this function. Reading X-Forwarded-For unconditionally hands attackers a free reset on all of them.
Rate limits, multi-account checks, one-claim-per-visitor caps: they all key off an IP address. If that address comes from a header the client controls, an attacker sets a new one on every request and every limit you wrote evaporates silently. Nothing errors. The numbers just stop meaning anything.
X-Forwarded-For is only trustworthy when a proxy you control sets it. That is a configuration fact about your install, so it belongs behind a setting, defaulted to off.
function client_ip(bool $trustProxy = false): string
{
$remote = (string) ($_SERVER['REMOTE_ADDR'] ?? '');
if ($trustProxy) {
// Cloudflare first: it is set by the edge and cannot be spoofed past it.
$cf = (string) ($_SERVER['HTTP_CF_CONNECTING_IP'] ?? '');
if (filter_var($cf, FILTER_VALIDATE_IP)) {
return $cf;
}
$xff = (string) ($_SERVER['HTTP_X_FORWARDED_FOR'] ?? '');
if ($xff !== '') {
// Left-most entry is the original client, if the chain is honest.
$first = trim(explode(',', $xff)[0]);
if (filter_var($first, FILTER_VALIDATE_IP)) {
return $first;
}
}
}
return filter_var($remote, FILTER_VALIDATE_IP) ? $remote : '0.0.0.0';
}
Using it
Turn trustProxy on only when the site actually sits behind Cloudflare or a load balancer you configured. On plain shared hosting, leave it off.
Use one function everywhere. The moment two places disagree about what the client IP is, your fraud numbers become fiction.
What bites people
Do not take the last entry in X-Forwarded-For instead of the first. The chain is appended to, and a client can seed it with anything.
IPv6 users rotate addresses more freely than IPv4 users. If you are clustering accounts by IP, treat a /64 as the unit rather than the exact address.