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
| Directive | Description | Example |
|---|---|---|
| `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.