When building multi-tenant SaaS routing or API gateways, reverse proxies (Nginx, Caddy, Envoy, Traefik) map inbound domain names to internal upstream services.
However, if your proxy blindly passes untrusted client headers upstream without strict hostname validation, you open the door to Host Header Poisoning, password reset URL hijacking, and web cache poisoning.
Here are the four defensive patterns to eliminate host header vulnerabilities at the edge.
1. The Vulnerability: Blind Header Passthrough
Consider a typical naive Nginx reverse proxy configuration:
server {
listen 80;
server_name _;
location / {
proxy_pass http://internal_app_upstream;
proxy_set_header Host $http_host; # DANGEROUS
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
If an attacker sends:
GET /auth/password-reset HTTP/1.1
Host: evil-attacker-domain.com
X-Forwarded-Host: evil-attacker-domain.com
The downstream application generates password reset links using the client-supplied Host header:
https://evil-attacker-domain.com/reset?token=xyz
When the user clicks the link, their reset token is logged on the attacker's server.
2. Pattern 1: Catch-All Default Server Drop
Never let a catch-all virtual host (server_name _) proxy requests downstream. Reject unmatched Host headers immediately with HTTP 444 (close connection) or 400 Bad Request.
# Reject any connection where Host does not match known domains
server {
listen 80 default_server;
listen 443 ssl default_server;
ssl_reject_handshake on; # Disconnect TLS before SNI negotiation
return 444;
}
3. Pattern 2: Strict Tenant Host Allowlisting
For dynamic multi-tenant platforms where tenants bring custom vanity domains, validate the host against an in-memory Redis or SQLite allowlist before proxying.
In Nginx with the njs scripting module or Lua:
# Map incoming host against known tenant domains
map $http_host $valid_tenant {
default 0;
app.example.com 1;
api.example.com 1;
include /etc/nginx/conf.d/allowed_custom_domains.map;
}
server {
listen 443 ssl;
server_name ~^(?<domain>.+)$;
if ($valid_tenant = 0) {
return 403 "Unknown or unverified domain host";
}
location / {
proxy_pass http://upstream_tenant_backend;
proxy_set_header Host $domain; # Explicitly set to validated variable
proxy_set_header X-Forwarded-Host $domain;
}
}
4. Pattern 3: Strip Spoofed Forwarded Headers
Internal microservices often inspect X-Forwarded-Host, X-Forwarded-Proto, or RFC 7239 Forwarded headers to determine the public-facing URL.
If your edge proxy does not sanitize inbound client values, external attackers can inject conflicting headers:
# Strip client-supplied forward headers and regenerate authoritative ones
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $remote_addr;
# Strip RFC 7239 header unless explicitly normalized
proxy_set_header Forwarded "";
5. Pattern 4: Web Cache Key Partitioning
If your edge layers cache dynamic responses, an attacker can poison the cache for all legitimate users by sending a request with a poisoned X-Forwarded-Host. If the cache key only includes the request path ($request_uri), subsequent users get the poisoned HTML.
Always ensure your cache key incorporates the fully qualified, verified scheme and host:
proxy_cache_key "$scheme$domain$request_uri";
Summary Checklist
- [ ] Does your reverse proxy reject unmatched SNI / Host headers with a default drop?
- [ ] Are custom tenant domains verified against an explicit allowlist before traffic hits upstream handlers?
- [ ] Are inbound
ForwardedandX-Forwarded-Hostheaders stripped and regenerated? - [ ] Does the web cache key include the validated domain?
Edge security begins by treating every inbound HTTP header as untrusted user input.
Run an instant conversion, SEO & leak audit on your website free: https://leaksift.pages.dev/?src=devto
Top comments (0)