How to correctly detect user IP address
Why REMOTE_ADDR is often wrong
When your app is behind Cloudflare, Nginx, ALB, or ingress, REMOTE_ADDR frequently contains proxy address. For antifraud this produces incorrect geo and risk scoring.
Safe extraction order
- Define trusted proxy addresses.
- If request is not from trusted proxy, use
REMOTE_ADDR. - If request is from trusted proxy, read
CF-Connecting-IP, thenX-Real-IP, then first valid IP fromX-Forwarded-For. - Validate every candidate IP.
- Log both selected IP and source header for audit.
Nginx example
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
real_ip_header CF-Connecting-IP;
real_ip_recursive on;
Node.js
function getClientIp(req, trusted) {
const remote = req.socket?.remoteAddress || "";
if (!trusted.has(remote)) return remote;
const cf = req.header("cf-connecting-ip");
if (cf && net.isIP(cf)) return cf;
const xr = req.header("x-real-ip");
if (xr && net.isIP(xr)) return xr;
const xff = req.header("x-forwarded-for") || "";
for (const part of xff.split(",").map(x => x.trim())) {
if (net.isIP(part)) return part;
}
return remote;
}
Python
def get_client_ip(request, trusted_proxies):
remote = request.client.host if request.client else ""
if remote not in trusted_proxies:
return remote
for header in ["cf-connecting-ip", "x-real-ip"]:
value = request.headers.get(header, "")
if is_valid_ip(value):
return value
for part in request.headers.get("x-forwarded-for", "").split(","):
ip = part.strip()
if is_valid_ip(ip):
return ip
return remote
PHP
function get_client_ip(array $server, array $trusted): string {
$remote = (string)($server["REMOTE_ADDR"] ?? "");
if (!in_array($remote, $trusted, true)) return $remote;
$cf = trim((string)($server["HTTP_CF_CONNECTING_IP"] ?? ""));
if (filter_var($cf, FILTER_VALIDATE_IP)) return $cf;
$xr = trim((string)($server["HTTP_X_REAL_IP"] ?? ""));
if (filter_var($xr, FILTER_VALIDATE_IP)) return $xr;
foreach (array_map("trim", explode(",", (string)($server["HTTP_X_FORWARDED_FOR"] ?? ""))) as $part) {
if (filter_var($part, FILTER_VALIDATE_IP)) return $part;
}
return $remote;
}
Checklist
- Application is not directly accessible from public internet.
- Trusted proxy list is maintained and monitored.
- Invalid or private addresses are filtered according to policy.
- Unit and integration tests cover header combinations.
API call
GET /api/?ip=203.0.113.42&user_id=uid_v1_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Authorization: Bearer YOUR_API_KEY