HTTP Fundamentals

HTTP Proxy Headers: X-Forwarded-For, Via, and Forwarded

How proxy and reverse proxy headers work, the Forwarded standard header (RFC 7239), and common pitfalls with IP spoofing and trust chains.

Why Proxy Headers Exist

When an HTTP request passes through a reverse proxy, load balancer, or CDN, the server loses visibility into the original client. From the application's perspective, every request appears to come from the proxy's IP address — making authentication, rate limiting, logging, and geo-restriction impossible without additional information.

Proxy headers solve this by having intermediaries append client connection information before forwarding the request upstream.

A typical path:

Client (1.2.3.4)
  → CDN Edge (10.0.0.1)
  → Load Balancer (10.0.0.2)
  → Application Server

Without proxy headers, the application only sees 10.0.0.2. With them, it can reconstruct the full path and determine the real client IP.

Legacy Headers

Before standardization, multiple non-standard headers emerged organically:

X-Forwarded-For

The most widely deployed proxy header. Each proxy appends the IP it received the request from:

# Client → Proxy A → Proxy B → Server
# Proxy A adds: X-Forwarded-For: 1.2.3.4
# Proxy B appends: X-Forwarded-For: 1.2.3.4, 10.0.0.1

X-Forwarded-For: 1.2.3.4, 10.0.0.1

The leftmost IP is the original client (or the first untrusted party to add the header). However, this is also the easiest field to spoof — clients can send arbitrary X-Forwarded-For values before the request reaches any trusted proxy.

X-Forwarded-Proto

Records the original protocol used by the client, since the proxy may connect to the backend over HTTP even when the client used HTTPS:

X-Forwarded-Proto: https

X-Forwarded-Host

The Host header as the client sent it, before any rewriting by the proxy:

X-Forwarded-Host: api.example.com

X-Real-IP

A single-value alternative to X-Forwarded-For. Set by the closest trusted proxy to the application, it avoids the parsing complexity of a comma-separated list but loses intermediate hop information:

X-Real-IP: 1.2.3.4

The Forwarded Standard (RFC 7239)

Published in 2014, RFC 7239 standardizes proxy information into a single Forwarded header with structured syntax and defined directives:

Forwarded: for=1.2.3.4;proto=https;host=api.example.com, for=10.0.0.1

Directives

DirectiveDescriptionExample
`for`Client or proxy IP`for=1.2.3.4`
`by`Interface the proxy received on`by=10.0.0.1`
`host`Original Host header`host=api.example.com`
`proto`Original protocol`proto=https`

IPv6 addresses and obfuscated identifiers are also supported:

Forwarded: for="[2001:db8::1]";proto=https
Forwarded: for=_hidden;proto=https   # Obfuscated for privacy
Forwarded: for=unknown;proto=https   # Identity unknown

Multiple proxies each append their own Forwarded value, separated by commas — the same chaining model as X-Forwarded-For.

Security Considerations

IP Spoofing via X-Forwarded-For

A malicious client can send a crafted X-Forwarded-For header before the request reaches your infrastructure:

# Attacker bypasses IP-based rate limiting:
curl -H 'X-Forwarded-For: 8.8.8.8' https://api.example.com/login

If your application naively reads the leftmost IP from X-Forwarded-For, it reads the attacker-controlled value. The fix: only trust IPs from known, trusted proxy positions.

Trusted Proxy Configuration

Read from the rightmost IP in the chain, working leftward only as far as you have trusted proxies:

# Django: configure trusted proxies (Django 4.0+)
# settings.py
USE_X_FORWARDED_HOST = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')

# For IP resolution, use a library like django-ipware:
from ipware import get_client_ip

def get_real_ip(request):
    ip, is_routable = get_client_ip(request)
    return ip  # Handles trusted proxy chain correctly
# Nginx: set real_ip_header and trusted proxy ranges
real_ip_header X-Forwarded-For;
real_ip_recursive on;
set_real_ip_from 10.0.0.0/8;      # Trust your load balancer subnet
set_real_ip_from 172.16.0.0/12;
set_real_ip_from 192.168.0.0/16;

SSRF via Host Header Injection

X-Forwarded-Host injection can cause Server-Side Request Forgery if your application uses this header to construct redirect URLs or generate links:

# VULNERABLE — attacker sends X-Forwarded-Host: evil.com
redirect_url = f"https://{request.META.get('HTTP_X_FORWARDED_HOST')}/reset?token=..."

# SAFE — use settings.ALLOWED_HOSTS validation
from django.contrib.sites.shortcuts import get_current_site
site = get_current_site(request)
redirect_url = f"https://{site.domain}/reset?token=..."

Framework Configuration Examples

Express (Node.js)

// Tell Express how many proxy hops to trust
app.set('trust proxy', 1);          // Trust 1 hop (e.g., Nginx)
app.set('trust proxy', 'loopback');  // Trust loopback addresses
app.set('trust proxy', '10.0.0.0/8'); // Trust by CIDR

// Now req.ip returns the client IP correctly
app.get('/', (req, res) => {
  console.log(req.ip);  // Real client IP
});

The Via Header

The standard Via header (RFC 7230) is set by forward proxies and records each proxy that handled the request — used for loop detection and protocol version tracking:

Via: 1.1 proxy1.example.com, 1.1 cdn-edge-node.cloudflare.com

Unlike X-Forwarded-For, Via is a standard header defined in the HTTP spec itself and is commonly stripped or anonymized by CDNs for privacy.

Giao thức liên quan

Thuật ngữ liên quan

Thêm trong HTTP Fundamentals