AuditCoreAuditCore
highCWE-644OWASP A04:2021 — Insecure Design

How to fix Host Header Injection

User clicks 'forgot password'. Your app emails them a reset link with hostname taken from the request's Host header. Attacker sends a request with `Host: evil.com` while spoofing the victim's email. Victim gets reset link pointing to evil.com — clicks it — password reset token sent to evil.com. Account takeover. Fix is hostname allow-listing.

What is Host Header Injection?

Host header injection happens when an app uses the request's Host header (or X-Forwarded-Host, X-Host, X-Forwarded-Server) to construct URLs without validation. The attacker controls these headers — they're set by the client, then forwarded by reverse proxies (often unchanged).

Three main exploits. Password reset poisoning: app builds reset URL from Host header, sends to victim's email; victim clicks link to attacker's domain; token leaks. Web cache poisoning: CDN caches response with attacker-controlled Host content (e.g., poisoned `<meta>` tags with attacker URLs); subsequent users get the poisoned cached response. SSRF via Host: app uses Host to construct internal URLs (`http://${host}/api/internal`); attacker sets Host to internal IP.

The fix pattern is the same as for SSRF and path traversal: don't trust user-controlled input as a URL component. Use a configured trusted hostname (or short allow-list) regardless of what the request says. Most frameworks have a setting for this — Django ALLOWED_HOSTS, Rails config.hosts, Next.js's hostname config. Use it.

What an attacker can do

The concrete impact of leaving Host Header Injection unpatched.

Password reset poisoning → account takeover

Attacker requests reset for victim's email with Host=evil.com. Victim clicks link to evil.com. Token leaked. Account takeover with no further interaction needed.

Cache poisoning serving attacker content

CDN caches poisoned response. Every subsequent visitor sees attacker's links/scripts/redirects under your origin.

SSRF via Host-based internal URLs

Apps that resolve Host into internal service URLs become SSRF vectors when attacker provides internal IP as Host.

OAuth redirect bypass

Apps building OAuth callback URLs from Host can be tricked into redirecting tokens to attacker domains.

How do I know if I'm vulnerable?

Manual: search for `request.host`, `req.headers.host`, `request.get_host()`, `request.url.host`, `X-Forwarded-Host`, `Forwarded` header parsing. Each use needs validation against an allow-list.

Automated: AuditCore's Host Header Scanner sends requests with `Host: evil.com`, `X-Forwarded-Host: evil.com`, ambiguous `Host` (multiple values), and observes which endpoints reflect the malicious host in responses (especially URLs in JSON, password reset links).

How to fix Host Header Injection

5 ordered steps. Apply them in order — each builds on the previous.

  1. 1

    Configure framework's allowed hosts list

    Most frameworks have built-in ALLOWED_HOSTS validation. Use it — requests with non-matching Host get rejected at the framework boundary.

    Django: ALLOWED_HOSTS. Rails: config.hosts. Express: configure trust proxy + custom middleware. Spring Boot: configure server.servlet.context.host.

    pythonStep 1
    # Django settings.py
    ALLOWED_HOSTS = ["app.example.com", "example.com", ".example.com"]
    USE_X_FORWARDED_HOST = True  # only if behind a trusted proxy
    USE_X_FORWARDED_PORT = True
    
    # Rails config/application.rb
    config.hosts << "app.example.com"
    config.hosts << "example.com"
    config.host_authorization = { exclude: ->(req) { req.path =~ /\.well-known/ } }
  2. 2

    Use a configured base URL — never derive from Host header

    When constructing absolute URLs (password reset, OAuth callbacks, email links), use a configured BASE_URL env var. Don't read Host.

    Same URL for everyone. If you have multiple deployments (staging, prod), each gets its own BASE_URL.

    javascriptStep 2
    // ❌ Vulnerable
    function passwordResetUrl(token, req) {
      return `https://${req.headers.host}/reset?token=${token}`;
      // attacker sets Host: evil.com → URL points to evil.com
    }
    
    // ✅ Fixed
    const BASE_URL = process.env.BASE_URL;  // "https://app.example.com"
    function passwordResetUrl(token) {
      return `${BASE_URL}/reset?token=${token}`;
    }
    
    // In email-sending code:
    sendEmail(user.email, `Reset your password: ${passwordResetUrl(token)}`);
  3. 3

    Reject ambiguous / multiple Host headers at the proxy layer

    Some attacks send `Host: evil.com\r\nHost: app.example.com` to confuse middleboxes. Reverse proxies should normalize.

    nginx by default rejects requests with multiple Host headers. Cloudflare, AWS ALB do the same. Verify your stack.

    nginxStep 3
    # nginx — explicit server_name + reject unknown hosts
    server {
        listen 443 ssl http2;
        server_name app.example.com;
        # ... routes ...
    }
    
    # Catch-all server block to reject other hostnames (return 444)
    server {
        listen 443 ssl http2 default_server;
        server_name _;
        ssl_certificate /etc/ssl/dummy.crt;
        ssl_certificate_key /etc/ssl/dummy.key;
        return 444;  # close connection without response
    }
  4. 4

    Configure cache key to include Host (not just URL)

    If you have caching (CDN, app-level), include Host in cache key. Otherwise poisoned response from one Host value pollutes other Hosts.

    CloudFront: include Host in cache key behavior. nginx fastcgi_cache: include $host in cache key. Default is usually OK in single-tenant; verify if multi-tenant or multi-domain.

    nginxStep 4
    # nginx fastcgi_cache key includes Host
    fastcgi_cache_key "$scheme$request_method$host$request_uri";
    
    # Or for proxy_cache:
    proxy_cache_key "$scheme$request_method$host$request_uri";
  5. 5

    Audit X-Forwarded-Host trust if behind multiple proxies

    If your stack is Client → Cloudflare → ALB → app, the X-Forwarded-Host can be set by any of them. Trust only the immediate proxy's headers.

    Configure framework to read only the rightmost trusted X-Forwarded-Host entry. Never trust an arbitrary count of forwards.

    javascriptStep 5
    // Express — trust proxy hop count, not 'true' (which trusts all)
    const app = express();
    app.set("trust proxy", 1);  // trust 1 proxy hop (e.g., your ALB)
    // Now req.hostname comes from X-Forwarded-Host of the first proxy only
    
    // Custom middleware — verify host against allow-list
    const ALLOWED = new Set(["app.example.com", "example.com"]);
    app.use((req, res, next) => {
      if (!ALLOWED.has(req.hostname)) {
        return res.status(400).send("Invalid host");
      }
      next();
    });

How to verify the fix

AuditCore — Host Header Scanner sends `Host: evil.com`, `X-Forwarded-Host: evil.com`, and ambiguous Host headers; reports endpoints that reflect the malicious host or mention it in responses.

Manual: `curl -H 'Host: evil.com' https://app.example.com/forgot-password -d [email protected]`. Check the email victim receives — does the reset link point to evil.com? If yes, vulnerable. Repeat with `X-Forwarded-Host: evil.com` and your real Host header.

FAQ

Frequently asked questions

Doesn't HTTPS prevent host header attacks?+

No. TLS encrypts data in transit but doesn't validate the Host header semantically. Browser sends `Host: app.example.com` (the URL it visited), but anyone scripting requests can send any Host they want with `curl -H 'Host: ...'`.

Is X-Forwarded-Host always attacker-controlled?+

It's set by the immediate client connection. If you're behind a trusted proxy that overwrites/strips it, you can trust the proxy's value. Without that, treat it as user input.

What's web cache poisoning specifically?+

Attacker sends a request with malicious Host. Server's response (with malicious URLs in HTML/headers) gets cached. Subsequent users on the same cache key receive the poisoned response. Real-world: PortSwigger has documented dozens of cache poisoning chains via Host header.

Should I use HSTS to prevent this?+

HSTS prevents downgrade attacks on the original Host. It doesn't prevent host header injection (server-side bug). HSTS is a separate defense for a different problem.

What about OAuth redirect_uri instead of Host?+

Different bug class but same family. OAuth redirect_uri also needs strict allow-listing. Both are 'don't trust client-supplied URLs in security-sensitive contexts'.

Don't just guess — scan and verify

AuditCore Free Trial scans your homepage for Host Header Injection and 50+ other vulnerability classes. No credit card. Results in 60 seconds.