Networking

X-Forwarded-For

An HTTP header that identifies the originating IP address of a client connecting through proxies or load balancers, preserving the client's real IP address through the proxy chain.

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();
});

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;
}

Learn More

Create a free Account to fix CORS Errors in Production

Say goodbye to CORS errors and get back to building great web applications. It's free!

CORSPROXY Dashboard

Related Terms

More in Networking

Related guides

Back to Glossary