What is X-Forwarded-For?
X-Forwarded-For (XFF) is a de-facto standard HTTP header used to identify the original IP address of a client connecting to a web server through an HTTP proxy or load balancer. When requests pass through proxies, the server sees the proxy’s IP instead of the client’s real IP. The X-Forwarded-For header preserves the client’s original IP address throughout the proxy chain.
Header Format
Syntax
X-Forwarded-For: <client-ip>, <proxy1-ip>, <proxy2-ip>, ...
Examples
# Single proxy
X-Forwarded-For: 203.0.113.195
# Multiple proxies (chain)
X-Forwarded-For: 203.0.113.195, 198.51.100.178, 192.0.2.100
# Multiple clients (comma-separated)
X-Forwarded-For: 203.0.113.195, 198.51.100.178
How X-Forwarded-For Works
Request Flow
Without Proxy:
Client (203.0.113.195) → Server
Server sees: 203.0.113.195
With Single Proxy:
Client (203.0.113.195) → Proxy (198.51.100.178) → Server
Server sees: 198.51.100.178
X-Forwarded-For: 203.0.113.195
With Multiple Proxies:
Client (203.0.113.195) → Proxy1 (198.51.100.178) → Proxy2 (192.0.2.100) → Server
Server sees: 192.0.2.100
X-Forwarded-For: 203.0.113.195, 198.51.100.178
Header Chain Building
interface XFFChainBuilding {
step1: {
description: 'Client connects to Proxy1';
headers: {};
clientIP: '203.0.113.195';
};
step2: {
description: 'Proxy1 adds X-Forwarded-For';
headers: {
'X-Forwarded-For': '203.0.113.195';
};
proxyIP: '198.51.100.178';
};
step3: {
description: 'Proxy2 appends to X-Forwarded-For';
headers: {
'X-Forwarded-For': '203.0.113.195, 198.51.100.178';
};
proxyIP: '192.0.2.100';
};
step4: {
description: 'Server receives request';
headers: {
'X-Forwarded-For': '203.0.113.195, 198.51.100.178';
};
directConnection: '192.0.2.100';
};
}
Parsing X-Forwarded-For
Extract Client IP
function getClientIP(req: any): string {
// Check X-Forwarded-For header
const xForwardedFor = req.headers['x-forwarded-for'];
if (xForwardedFor) {
// XFF contains comma-separated list of IPs
// First IP is the original client
const ips = xForwardedFor.split(',').map((ip: string) => ip.trim());
return ips[0];
}
// Fallback to direct connection IP
return req.socket.remoteAddress || 'unknown';
}
// Usage in Express
import express from 'express';
const app = express();
app.get('/api/data', (req, res) => {
const clientIP = getClientIP(req);
console.log(`Request from: ${clientIP}`);
res.json({
clientIP,
headers: req.headers
});
});
Validate IP Addresses
function isValidIP(ip: string): boolean {
// IPv4 validation
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
if (ipv4Regex.test(ip)) {
const parts = ip.split('.').map(Number);
return parts.every(part => part >= 0 && part <= 255);
}
// IPv6 validation (simplified)
const ipv6Regex = /^([0-9a-fA-F]{0,4}:){7}[0-9a-fA-F]{0,4}$/;
return ipv6Regex.test(ip);
}
function parseXForwardedFor(xffHeader: string): {
clientIP: string | null;
proxyChain: string[];
valid: boolean;
} {
if (!xffHeader) {
return {
clientIP: null,
proxyChain: [],
valid: false
};
}
const ips = xffHeader.split(',').map(ip => ip.trim());
// Validate all IPs
const validIPs = ips.filter(isValidIP);
if (validIPs.length === 0) {
return {
clientIP: null,
proxyChain: [],
valid: false
};
}
return {
clientIP: validIPs[0],
proxyChain: validIPs.slice(1),
valid: true
};
}
// Usage
const result = parseXForwardedFor('203.0.113.195, 198.51.100.178, 192.0.2.100');
console.log(result);
/*
{
clientIP: '203.0.113.195',
proxyChain: ['198.51.100.178', '192.0.2.100'],
valid: true
}
*/
Setting X-Forwarded-For
Proxy Server Implementation
import http from 'http';
import httpProxy from 'http-proxy';
class ProxyWithXFF {
private proxy: httpProxy;
constructor() {
this.proxy = httpProxy.createProxyServer({});
}
start(port: number) {
const server = http.createServer((req, res) => {
this.handleRequest(req, res);
});
server.listen(port, () => {
console.log(`Proxy with XFF listening on port ${port}`);
});
return server;
}
private handleRequest(
req: http.IncomingMessage,
res: http.ServerResponse
) {
const targetUrl = req.url;
if (!targetUrl) {
res.writeHead(400);
res.end('Bad Request');
return;
}
// Get client IP
const clientIP = this.getClientIP(req);
// Add or append to X-Forwarded-For
const existingXFF = req.headers['x-forwarded-for'];
if (existingXFF) {
// Append to existing chain
if (Array.isArray(existingXFF)) {
req.headers['x-forwarded-for'] = `${existingXFF.join(', ')}, ${clientIP}`;
} else {
req.headers['x-forwarded-for'] = `${existingXFF}, ${clientIP}`;
}
} else {
// Create new XFF header
req.headers['x-forwarded-for'] = clientIP;
}
// Add other forwarded headers
req.headers['x-real-ip'] = clientIP;
req.headers['x-forwarded-proto'] = req.socket.encrypted ? 'https' : 'http';
req.headers['x-forwarded-host'] = req.headers.host || '';
console.log(`Proxying: ${clientIP} → ${targetUrl}`);
console.log(`X-Forwarded-For: ${req.headers['x-forwarded-for']}`);
// Forward request
this.proxy.web(req, res, {
target: targetUrl,
changeOrigin: true
});
}
private getClientIP(req: http.IncomingMessage): string {
// Get direct connection IP
return req.socket.remoteAddress || 'unknown';
}
}
// Usage
const proxy = new ProxyWithXFF();
proxy.start(8080);
Nginx Configuration
server {
listen 80;
server_name example.com;
location / {
# Pass request to backend
proxy_pass http://backend:3000;
# Preserve client IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
# Other proxy settings
proxy_set_header Host $host;
proxy_set_header Connection "";
proxy_http_version 1.1;
}
}
# $proxy_add_x_forwarded_for automatically handles:
# - Creating new XFF header if none exists
# - Appending to existing XFF header
# Value: $http_x_forwarded_for, $remote_addr
Security Considerations
IP Spoofing
class SecureXFFHandler {
private trustedProxies: Set<string>;
constructor(trustedProxies: string[]) {
this.trustedProxies = new Set(trustedProxies);
}
getTrustedClientIP(req: any): string {
const directIP = req.socket.remoteAddress;
// If request is not from trusted proxy, use direct IP
if (!this.trustedProxies.has(directIP)) {
return directIP;
}
// Request is from trusted proxy, check XFF
const xffHeader = req.headers['x-forwarded-for'];
if (!xffHeader) {
return directIP;
}
// Parse XFF from right to left (most recent proxy first)
const ips = xffHeader.split(',').map((ip: string) => ip.trim());
// Find first untrusted IP (this is the real client)
for (let i = ips.length - 1; i >= 0; i--) {
const ip = ips[i];
if (!this.trustedProxies.has(ip)) {
// This is the client IP
return ip;
}
}
// All IPs are trusted proxies, use first IP
return ips[0];
}
validateXFF(req: any): {
valid: boolean;
reason?: string;
} {
const xffHeader = req.headers['x-forwarded-for'];
if (!xffHeader) {
return { valid: true };
}
const ips = xffHeader.split(',').map((ip: string) => ip.trim());
// Check for obviously spoofed IPs
for (const ip of ips) {
if (!isValidIP(ip)) {
return {
valid: false,
reason: `Invalid IP in XFF: ${ip}`
};
}
if (this.isPrivateIP(ip)) {
return {
valid: false,
reason: `Private IP in XFF: ${ip}`
};
}
}
// Check chain length (prevent abuse)
if (ips.length > 10) {
return {
valid: false,
reason: 'XFF chain too long (possible attack)'
};
}
return { valid: true };
}
private isPrivateIP(ip: string): boolean {
// Check if IP is private (RFC 1918)
const parts = ip.split('.').map(Number);
if (parts.length !== 4) return false;
const [a, b] = parts;
// 10.0.0.0/8
if (a === 10) return true;
// 172.16.0.0/12
if (a === 172 && b >= 16 && b <= 31) return true;
// 192.168.0.0/16
if (a === 192 && b === 168) return true;
// 127.0.0.0/8 (localhost)
if (a === 127) return true;
return false;
}
}
// Usage
const handler = new SecureXFFHandler([
'192.0.2.100', // Trusted load balancer
'192.0.2.101' // Trusted proxy
]);
function handleRequest(req: any, res: any) {
// Validate XFF
const validation = handler.validateXFF(req);
if (!validation.valid) {
console.error('Invalid XFF:', validation.reason);
res.status(400).send('Bad Request');
return;
}
// Get trusted client IP
const clientIP = handler.getTrustedClientIP(req);
console.log(`Real client IP: ${clientIP}`);
res.send({ clientIP });
}
Rate Limiting by Client IP
interface RateLimitEntry {
requests: number;
resetTime: number;
}
class IPRateLimiter {
private limits: Map<string, RateLimitEntry>;
private maxRequests: number;
private windowMs: number;
private xffHandler: SecureXFFHandler;
constructor(
maxRequests: number,
windowMs: number,
trustedProxies: string[]
) {
this.limits = new Map();
this.maxRequests = maxRequests;
this.windowMs = windowMs;
this.xffHandler = new SecureXFFHandler(trustedProxies);
}
checkLimit(req: any): {
allowed: boolean;
remaining: number;
resetIn: number;
} {
// Get real client IP from XFF
const clientIP = this.xffHandler.getTrustedClientIP(req);
const now = Date.now();
let entry = this.limits.get(clientIP);
// Create or reset entry
if (!entry || now > entry.resetTime) {
entry = {
requests: 0,
resetTime: now + this.windowMs
};
this.limits.set(clientIP, entry);
}
// Increment counter
entry.requests++;
const allowed = entry.requests <= this.maxRequests;
const remaining = Math.max(0, this.maxRequests - entry.requests);
const resetIn = entry.resetTime - now;
return {
allowed,
remaining,
resetIn
};
}
}
// Usage in Express middleware
import express from 'express';
const app = express();
const rateLimiter = new IPRateLimiter(
100, // 100 requests
60000, // per minute
['192.0.2.100'] // trusted proxies
);
app.use((req, res, next) => {
const result = rateLimiter.checkLimit(req);
res.setHeader('X-RateLimit-Limit', '100');
res.setHeader('X-RateLimit-Remaining', result.remaining.toString());
res.setHeader('X-RateLimit-Reset', new Date(Date.now() + result.resetIn).toISOString());
if (!result.allowed) {
res.status(429).send('Too Many Requests');
return;
}
next();
});
Related Headers
Forwarded Header (RFC 7239)
interface ForwardedHeader {
standard: 'RFC 7239 - standardized version';
format: 'Forwarded: for=client;host=example.com;proto=https';
example: 'Forwarded: for=203.0.113.195;proto=https;host=example.com';
}
function parseForwardedHeader(forwarded: string): {
for?: string;
host?: string;
proto?: string;
by?: string;
} {
const result: any = {};
// Parse Forwarded header
const pairs = forwarded.split(';');
pairs.forEach(pair => {
const [key, value] = pair.split('=').map(s => s.trim());
if (key && value) {
// Remove quotes if present
result[key] = value.replace(/"/g, '');
}
});
return result;
}
// Modern approach: use Forwarded instead of X-Forwarded-*
const forwarded = parseForwardedHeader(
'for=203.0.113.195;proto=https;host=example.com'
);
console.log(forwarded);
// { for: '203.0.113.195', proto: 'https', host: 'example.com' }
X-Real-IP
interface XRealIP {
purpose: 'Alternative to X-Forwarded-For';
format: 'X-Real-IP: <single-ip>';
limitation: 'Only contains one IP (client IP)';
usage: 'Common in Nginx, Cloudflare';
}
function getClientIPWithFallback(req: any): string {
// Priority order:
// 1. X-Real-IP (single IP, most reliable if from trusted proxy)
// 2. X-Forwarded-For (first IP)
// 3. Direct connection
const xRealIP = req.headers['x-real-ip'];
if (xRealIP && typeof xRealIP === 'string') {
return xRealIP;
}
const xForwardedFor = req.headers['x-forwarded-for'];
if (xForwardedFor) {
const ips = (typeof xForwardedFor === 'string' ? xForwardedFor : xForwardedFor[0])
.split(',')
.map(ip => ip.trim());
return ips[0];
}
return req.socket.remoteAddress || 'unknown';
}
Best Practices
For Proxy Operators
interface ProxyBestPractices {
alwaysAddXFF: {
rule: 'Always add or append to X-Forwarded-For';
reason: 'Preserve client IP through proxy chain';
implementation: 'Add client IP to XFF header';
};
preserveExisting: {
rule: 'Append to existing XFF, don\'t replace';
reason: 'Maintain complete proxy chain';
implementation: 'Check if XFF exists, append if so';
};
addRelatedHeaders: {
rule: 'Add X-Real-IP, X-Forwarded-Proto, X-Forwarded-Host';
reason: 'Provide complete forwarding context';
implementation: 'Set all X-Forwarded-* headers';
};
validateBeforeForwarding: {
rule: 'Validate IP addresses in XFF';
reason: 'Prevent injection attacks';
implementation: 'Check IP format and length';
};
}
For Application Developers
interface AppBestPractices {
trustConfiguration: {
rule: 'Configure trusted proxy IPs';
reason: 'Prevent IP spoofing';
implementation: 'Maintain whitelist of trusted proxies';
};
parseFromRight: {
rule: 'Parse XFF from right to left';
reason: 'Find first untrusted IP';
implementation: 'Traverse array backwards';
};
validateIPs: {
rule: 'Validate all IPs in chain';
reason: 'Detect malformed or spoofed headers';
implementation: 'Use IP validation library';
};
limitChainLength: {
rule: 'Reject excessively long XFF chains';
reason: 'Prevent DoS attacks';
implementation: 'Max 10 IPs in chain';
};
logForAuditing: {
rule: 'Log both direct IP and XFF';
reason: 'Security auditing and debugging';
implementation: 'Include in access logs';
};
}
Common Issues
Issue 1: Spoofed X-Forwarded-For
// Problem: Attacker sends fake XFF header
// Request: X-Forwarded-For: 1.2.3.4 (spoofed)
// Direct IP: 5.6.7.8 (attacker)
// Solution: Only trust XFF from trusted proxies
function getClientIPSecurely(req: any, trustedProxies: string[]): string {
const directIP = req.socket.remoteAddress;
// If not from trusted proxy, ignore XFF
if (!trustedProxies.includes(directIP)) {
return directIP; // Use direct IP, ignore spoofed XFF
}
// From trusted proxy, use XFF
const xff = req.headers['x-forwarded-for'];
if (xff) {
return xff.split(',')[0].trim();
}
return directIP;
}
Issue 2: Multiple X-Forwarded-For Headers
// Problem: Multiple XFF headers instead of comma-separated
// X-Forwarded-For: 1.2.3.4
// X-Forwarded-For: 5.6.7.8
// Solution: Combine multiple headers
function combineXFFHeaders(req: any): string | null {
const xff = req.headers['x-forwarded-for'];
if (!xff) {
return null;
}
if (Array.isArray(xff)) {
// Multiple headers - combine them
return xff.join(', ');
}
return xff;
}