Authority Ambiguity Vulnerabilities in NGINX and Debian’s proxy_params
Friends don't let friends use $http_host
Published:
Updated:
Two related authority-ambiguity vulnerabilities in NGINX and Debian's proxy_params configuration snippet.
NGINX is vulnerable to authority-ambiguity (or, more colloquially, host-ambiguity) attacks. An attacker can craft a HTTP/1.x request with conflicting authority values, where the authority contained in the request line conflicts with the authority in the Host header field. This makes NGINX use two different authorities for virtual host dispatching on the one hand and for other purposes, such as in $http_host NGINX variable or HTTP_HOST CGI variable (when using FastCGI, SCGI or uWSGI), on the other hand. In some deployments, authority-ambiguous requests could be used to bypass access control restrictions or for cache poisoning. Debian's suggested reverse proxy configuration (proxy_params) previously used $http_host, potentially exposing applications to these risks.
Table of content
- Table of content
- Authority-ambiguous requests in NGINX
- Authority-ambiguity vulnerability through $http_host NGINX variable
- Authority ambiguity vulnerability through HTTP_HOST CGI variable
- Appendix, Debian packages
- Appendix, how to send an authority-ambiguous request
- Appendix, what specifications say
- Appendix, description of the NGINX variables
- References
Authority-ambiguous requests in NGINX
When receiving a HTTP/1.x request, NGINX does not check that the authority from the request line (if present) is consistent with the authority from the Host header field. Such authority-ambiguous requests are accepted by NGINX without normalization.
Example of authority-ambiguity in HTTP request:
GET http://tenant1/ HTTP/1.1
User-Agent: UA
Host: tenant2
An attacker may be able to use this authority ambiguity. This may be possible:
- When the
$http_hostvariable is used. This was the case in Debian'sproxy_paramssnippet. - When using FastCGi, uWSGI or SCGI, because the value of the incoming
Hostheader field was passed by default in theHTTP_HOSTCGI variable.
Resolution:
- The
$http_hostusage inproxy_paramsis fixed in Debian package 1.26.3-3+deb13u4. - The
$http_hostissue it not fixed by NGINX. - The
HTTP_HOSTis mitigated in Debian package since 1.26.3-3+deb13u5 (infastcgi_params, etc.). - The
HTTP_HOSTissue is fixed in NGINX 1.29.5.
Some potential impacts (discussed in more details below):
- bypass security restrictions configured in NGINX through virtual host confusion;
- cache poisoning through virtual host confusion.
Note: authority in the header line?
The typical HTTP/1.x request (eg. for http://bar/) looks like:
GET / HTTP/1.1
User-Agent: UA
Host: bar
...
The Host header field is used to transmit the requested authority which makes it possible to support virtual hosts.
However, HTTP/1.x also allows to pass the authority as part of the request line:
GET http://bar/ HTTP/1.1
User-Agent: UA
Host: bar
...
This form is actually expected to be used when the client is talking to a (forward) proxy.
What is supposed to happen an authority is present in the request line?
- This is intended to be used when talking to a proxy server.
- An origin server is expected to accept it anyway.
- Even if the authority in present in the request line, it must be repeated in the
Hostheader field (with the same value). - The server must ignore the
Hostheader field if the authority is present in the request line.
Authority-ambiguity vulnerability through $http_host NGINX variable
Let us consider the previous example of authority-ambiguous HTTP request:
GET http://tenant1/ HTTP/1.1
User-Agent: UA
Host: tenant2
NGINX uses the authority from the request line, if present, for virtual host dispatching and the Host header field otherwise. On the other hand, the $http_host NGINX variable is always populated from the (raw) Host header field.
In this case, the NGINX variables are set as:
$host = tenant1
$http_host = tenant2
$request = GET http://tenant1/ HTTP/1.1
Impact
Any usage of $http_host might lead to unexpected behavior in case of malicious host-ambiguous request. If you use $http_host in your configuration and if the NGINX server is directly exposed (not only reachable through another reverse proxy), you might want to evaluate the impact, if any, of a spoofed/malicious Host value.
Virtual host confusion
If we take the previous authority-ambiguous request, “tenant1” is used for virtual host routing but the request is forwarded to the backend server as intended to “tenant2”. The request sent by NGINX to the backend looks like:
GET / HTTP/1.1
User-Agent: UA
Host: tenant2
X-Real-IP: ...
X-Forwarded-For: ...
X-Forwarded-Proto: ...
If the backend application actually makes use of the Host value, this might have a security impact.
For example,
- if the backend application uses the
Hostvalue for tenant dispatching; - and NGINX is configured with per-tenant configuration (eg. authorization, logging, etc),
then this might have a security impact.
Let us take the following example,
server {
listen 443 ssl;
server_name tenant1;
ssl_certificate /etc/nginx/ssl/tenant1.crt;
ssl_certificate_key /etc/nginx/ssl/tenant1.key;
location / {
proxy_pass http://backend;
# pass $http_host as Host for virtual host dispatching in the backend:
include proxy_params;
}
}
server {
listen 443 ssl;
server_name tenant2;
ssl_certificate /etc/nginx/ssl/tenant2.crt;
ssl_certificate_key /etc/nginx/ssl/tenant2.key;
location / {
allow 10.0.0.0/8;
allow 192.168.0.0/16;
allow 127.0.0.1/8;
deny all;
proxy_pass http://backend;
# pass $http_host as Host for virtual host dispatching in the backend:
include proxy_params;
}
}
In this example, “tenant2” is expected to be only available from private IP addresses. However, an external attacker could target “tenant2” with the following request:
GET http://tenant1/ HTTP/1.1
User-Agent: UA
Host: tenant2
In this situation,
- NGINX uses the authority from the request line for virtual host routing and therefore routes the request to the “tenant1”
serverblock. As the request does not reach the “tenant2”serverblock, its access control directives are not applied. - NGINX passes “tenant2” to the backend (in the
Hostheader field). - Therefore the attacker is able to reach the backend of “tenant2”, while bypassing the restrictions which should be applied to this tenant.
Cache poisoning
Another interesting scenario involves cache poisoning.
By default, NGINX caches are keyed by $scheme$proxy_host$request_uri where $proxy_host is the hostname from the backend URI (proxy_pass).
If $http_host is used in the cache key
a authority-ambiguous request could cause responses from one virtual host to be cached under a cache key associated with another virtual host (cross-virtual-host cache poisoning / cache confusion).
For example, if we consider this configuration,
server {
server_name app1;
listen 443 ssl;
listen [::]:443;
proxy_pass http://backend1;
proxy_cache_key $scheme$http_host$request_uri;
# ...
}
server {
server_name app2;
listen 443 ssl;
listen [::]:443;
proxy_pass http://backend2;
proxy_cache_key $scheme$http_host$request_uri;
# ...
}
In this case, an attacker might be able to trigger cross-virtual-host cache poisoning with a request such as:
GET https://app1/xyz HTTP/1.1
Host: app2
UA: u
In this case:
- NGINX routes the request to
http://backend1. - However it populates the cache for
https://app2/xyz.
If $http_host is passed to the backend for tenant routing
server {
server_name app1;
listen 443 ssl;
listen [::]:443;
proxy_pass http://backend;
proxy_cache_key $scheme$host$request_uri;
# used for vhost routing by backend:
proxy_set_header Host $http_host;
# ...
}
server {
server_name app2;
listen 443 ssl;
listen [::]:443;
proxy_pass http://backend;
proxy_cache_key $scheme$host$request_uri;
# used for vhost routing by backend:
proxy_set_header Host $http_host;
# ...
}
In this case, an attacker could use the same HTTP request to reach the backend for app2 and the response (from app2) would then populate the cache for https://app1/xyz.
Breaking absolute links
If the Host header field if used by downstream application to generate absolute links, an attacker might be able to provider a malicious value to the backend application. This is especially interesting when combined with cache poisoning.
It is arguably a bad idea to use the content of the Host header field to generate absolute links.
Debian proxy_params
Debian's proxy_params (/etc/nginx/proxy_params) snippet[1] used to use $http_host:
proxy_set_header Host $http_host;
This has been fixed as part of Debian bug #1126960. The proxy_params snippet now uses:
proxy_set_header Host $host;
In Debian forky (currently testing), we have instead:
proxy_set_header Host $host$is_request_port$request_port;
Are you impacted?
You might be impacted if:
- NGINX is directly exposed (not behind a reverse proxy);
- the configuration uses
$http_host(eg. via Debian'sproxy_params).
NGINX's position
NGINX considers this a configuration problem and not a vulnerability in NGINX as their documentation recommends using $host over $http_host:
proxy_set_header Host $host;
Resolution
NGINX might consider:
- either reject authority ambiguous requests altogether;
- or override the HTTP header with the value from the request line.
These behaviors are consistent with what other (open-source) HTTP servers do:
- NGINX when using HTTP/2 and HA proxy for solution 1;
- Traefik, Caddy and Apache HTTPD for solution 2.
At the very least, the security impact of using $host vs $http_host should be better documented.
Mitigation
The $host variable should be used instead of $http_host:
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
This does not include the port which might break some applications.
If this is a problem, you can use instead (since nginx 1.29.3):
proxy_set_header Host $host$is_request_port$request_port;
proxy_set_header X-Forwarded-Host $host$is_request_port$request_port;
Alternatively, you could $server_name instead of $http_host,
proxy_set_header Host $server_name;
proxy_set_header X-Forwarded-Host $server_name;
The $server_name is the name of the server block (defined by the server_name directive). This does not include the port either. Moreover this sets *.example.com when using wildcards so this should not be used with wildcards in server_name.
Mitigation 1c: use the hardcoded expected hostname instead of $http_host,
proxy_set_header Host "www.example.com";
proxy_set_header X-Forwarded-Host "www.example.com";
Note: rejecting ambiguous requests
I initially considered rejecting ambiguous requests through a configuration such as:
if ($http_host != $host) {
return 421 "Ambiguous host";
}
However $host is converted to lowercase whereas $http_host. This configuration would reject a request sending the host name in uppercase which is not conforming.
Authority ambiguity vulnerability through HTTP_HOST CGI variable
Even if $http_host is not used, authority-ambiguous vulnerability might be introduced by NGINX FastCGI, SCGI and uWSGI support via the HTTP_HOST CGI variable. In this case, NGINX passes the HTTP Header field from the incoming request in the HTTP_HOST CGI variable.
Similar the the previous situation, Unless the HTTP_HOST variable is explicitly an attacker can set a HTTP_HOST parameter which is not consistent with the value used for virtual host routing unless the HTTP_HOST is overridden by some NGINX configuration.
GET http://tenant1/ HTTP/1.1 <- Used bby NGINX for virtual host routing
User-Agent: UA
Host: tenant2 <- Used by NGINX for HTTP_HOST
Leading to the following variables:
SERVER_NAME = tenant1
HTTP_HOST = tenant2 <- from Host header field
This is fixed in NGINX v1.29.5 and is mitigated in Debian package 1.26.3-3+deb13u.
Use of the HTTP_HOST CGI variable in applications
One could argue that application code should rely on SERVER_NAME (not HTTP_HOST) according to the CGI specification this is not what happens in practice:
- the usage of
HTTP_HOSTin application code is quite prevalent as well; SERVER_NAME/$server_namedoes not really do what we want when using wildcard server names.
For example the URL reconstruction algorithm documented in WSGI (Python) is:
from urllib.parse import quote
url = environ['wsgi.url_scheme']+'://'
if environ.get('HTTP_HOST'):
url += environ['HTTP_HOST']
else:
url += environ['SERVER_NAME']
if environ['wsgi.url_scheme'] == 'https':
if environ['SERVER_PORT'] != '443':
url += ':' + environ['SERVER_PORT']
else:
if environ['SERVER_PORT'] != '80':
url += ':' + environ['SERVER_PORT']
url += quote(environ.get('SCRIPT_NAME', ''))
url += quote(environ.get('PATH_INFO', ''))
if environ.get('QUERY_STRING'):
url += '?' + environ['QUERY_STRING']
With this algorithm, HTTP_HOST takes precedence over SERVER_NAME. The reconstructed URL is therefore vulnerable to authority ambiguity requests.
Similarly PSGI (Perl) says:
SERVER_NAME,SERVER_PORT: When combined withSCRIPT_NAMEandPATH_INFO, these keys can be used to complete the URL. Note, however, thatHTTP_HOST, if present, should be used in preference toSERVER_NAMEfor reconstructing the request URL.SERVER_NAMEandSERVER_PORTMUST NOT be empty strings, and are always required.
Impact
The impacts are similar to the previous section.
Are you impacted?
You might be impacted if:
- NGINX is directly exposed;
- you do not override the value of
HTTP_HOST; - the application actually uses
HTTP_HOST.
Resolution
This is fixed since NGINX v1.29.5. NGINX now passes the authority value from the request line (when present) in HTTP_HOST instead of the value of the Host header field.
Since NGINX v1.29.5, the HTTP_HOST variable is set to the authority of the request line by default:
SERVER_NAME = tenant1
HTTP_HOST = tenant1
Mitigation
This is mitigated in Debian package 1.26.3-3+deb13u (for older versions of NGINX) through fastcgi_params, scgi_params and uwsgi_param:
fastcgi_param HTTP_HOST $host;
scgi_param HTTP_HOST $host;
uwsgi_param HTTP_HOST $host;
This workaround is only applied when the relevant snippet is included. For example, for FastCGI:
include fastcgi_params;
As before, another mitigation could be to reject ambiguous requests (using return).
Appendix, Debian packages
Snippet from Debian changelogs:
nginx (1.26.3-3+deb13u4) trixie; urgency=medium
- d/conf/*_params: use "$host" instead of "$http_host"
- "$http_host" forwards the Host header exactly as supplied by the client and may not match the effective request target (e.g. absolute-form requests with a conflicting Host header) this can expose inconsistent or attacker-controlled host values to backend applications (uwsgi, fastcgi, scgi, proxy)
- switch to "$host" as a safer, normalized alternative
- note: this changes behaviour, as "$host" does not preserve the client-supplied port; deployments relying on "$http_host" including a port number may be affected
- it is workaround for Debian bug #1126960 for stable/oldstable release
-- Jan Mojžíš janmojzis@debian.org Mon, 20 Apr 2026 17:52:06 +0000
New proxy_params file (since 1.26.3-3+deb13u4):
# !!! Security workaround !!!
# Do not set the `Host` header as "$http_host".
#
# "$http_host" is the Host header exactly as supplied by the client.
# This is unsafe when a client sends an absolute-form request target together
# with a different Host header, for example:
#
# GET https://example.com/ HTTP/1.1
# Host: malformedhost
#
# In such a case, passing "$http_host" upstream exposes the raw client-supplied
# Host value ("malformedhost") to the backend application, even though it does
# not match the effective request target. Applications often use HTTP_HOST for
# redirects, absolute URL generation, virtual host routing, or security checks;
# forwarding the raw Host header can therefore lead to incorrect or unsafe
# behaviour.
#
# Newer nginx versions (since 1.30.0) introduce variables "$is_request_port" and
# "$request_port", allowing `Host` to be constructed as:
# $host$is_request_port$request_port
#
# In stable/oldstable packages we use "$host" as a security workaround.
# It avoids forwarding an untrusted raw Host header to the backend.
#
# Note: this changes behaviour compared to previous versions, because "$host"
# does not preserve the client-supplied port, while "$http_host" typically
# does. Existing deployments that rely on "$http_host" containing a port number
# may therefore break or behave differently after this change.
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;
Appendix, how to send an authority-ambiguous request
printf "GET http://tenant1/ HTTP/1.1\r\nUser-Agent: UA\r\nHost: tenant2\r\n\r\n" |
tee /dev/stderr |
nc 127.0.0.1 8000
Appendix, what specifications say
RFC 9112 (HTTP/1):
When an origin server receives a request with an absolute-form of request-target, the origin server MUST ignore the received
Hostheader field (if any) and instead use the host information of the request-target.
RFC 9113 (HTTP/2):
Clients MUST NOT generate a request with a Host header field that differs from the ":authority" pseudo-header field. A server SHOULD treat a request as malformed if it contains a Host header field that identifies an entity that differs from the entity in the ":authority" pseudo-header field.
RFC 9114 (HTTP/3):
If the
:schemepseudo-header field identifies a scheme that has a mandatory authority component (including "http" and "https"), the request MUST contain either an:authoritypseudo-header field or aHostheader field. If these fields are present, they MUST NOT be empty. If both fields are present, they MUST contain the same value.
RFC 3875 (CGI 1.1):
The
SERVER_NAMEvariable MUST be set to the name of the server host to which the client request is directed. [...] A deployed server can have more than one possible value for this variable, where several HTTP virtual hosts share the same IP address. In that case, the server would use the contents of the request's Host header field to select the correct virtual host.
WSGI (Python):
from urllib.parse import quote
url = environ['wsgi.url_scheme']+'://'
if environ.get('HTTP_HOST'):
url += environ['HTTP_HOST']
else:
url += environ['SERVER_NAME']
if environ['wsgi.url_scheme'] == 'https':
if environ['SERVER_PORT'] != '443':
url += ':' + environ['SERVER_PORT']
else:
if environ['SERVER_PORT'] != '80':
url += ':' + environ['SERVER_PORT']
url += quote(environ.get('SCRIPT_NAME', ''))
url += quote(environ.get('PATH_INFO', ''))
if environ.get('QUERY_STRING'):
url += '?' + environ['QUERY_STRING']
PSGI (Perl):
SERVER_NAME,SERVER_PORT: When combined withSCRIPT_NAMEandPATH_INFO, these keys can be used to complete the URL. Note, however, that HTTP_HOST, if present, should be used in preference toSERVER_NAMEfor reconstructing the request URL.SERVER_NAMEandSERVER_PORTMUST NOT be empty strings, and are always required.
Appendix, description of the NGINX variables
For the request,
GET http://HOST1:8080/ HTTP/1.1
User-Agent: UA
Host: HOST2
we have the variables
$host = host1
$http_host = HOST2
$scheme = http
$request_uri = /
$request = GET http://HOST1:8080/ HTTP/1.1
$request_port = 8080
$is_request_port = :
$server_name = (from server_name directive)
$host:
- hostname from the request line if present
- otherwise hostname from the
Hostheader field - same value used for virtual host routing
- converted to lowercase
- without the port
$http_host:
- always the value from the
Hostheader field - not converted to lowercase
- includes the port
- may not match with the host used for virtual host routing
- should usually not be used (use
$hostinstead)
$request:
- the raw request line (eg.
GET http://host1:9000/ HTTP/1.1) - not converted to lowercase lowercase
$request_port (since nginx 1.29.3):
- the port number from the request line (if present) or
Hostheader field - if present,
$is_request_port = :(used in$is_request_port$request_port)
$server_name:
- value from the (first)
server_namedirective (of the selectedserverblock) - when the server_name uses a wildcard (
server_name *.example;) the$server_nameincludes the wildcard ($server_name = *.example;)
References
HTTP specifications:
CGI and associated:
NGINX documentation:
About this vulnerability:
- Debian changelog for NGINX package
- Debian Bug 1126960, nginx: proxy_params should use $host instead of $http_host
- Thread on oss-security
Related security research:
- Virtual Host Confusion: Weaknesses and Exploits (Black Hat 2014)
This snippet is typically used like this:
server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name app.example.com; access_log /var/log/nginx/app.example.com.access.log; error_log /var/log/nginx/app.example.com.error.log error; location / { proxy_pass http://backend.example; include proxy_params; } ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem; }By default, NGINX passes the authority of the backend URI (from the
proxy_passdirective, in this casebackend.example) when used as a reverse proxy. Theproxy_set_header Host $http_host;directive attempts to pass the authority of the incoming HTTP request. ↩︎