/dev/posts/

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

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:

  1. When the $http_host variable is used. This was the case in Debian's proxy_params snippet.
  2. When using FastCGi, uWSGI or SCGI, because the value of the incoming Host header field was passed by default in the HTTP_HOST CGI variable.

Resolution:

Some potential impacts (discussed in more details below):

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 Host header field (with the same value).
  • The server must ignore the Host header 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,

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,

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:

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.

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

  1. either reject authority ambiguous requests altogether;
  2. or override the HTTP header with the value from the request line.

These behaviors are consistent with what other (open-source) HTTP servers do:

  1. NGINX when using HTTP/2 and HA proxy for solution 1;
  2. 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:

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 with SCRIPT_NAME and PATH_INFO, these keys can be used to complete the URL. Note, however, that HTTP_HOST, if present, should be used in preference to SERVER_NAME for reconstructing the request URL. SERVER_NAME and SERVER_PORT MUST 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:

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 Host header 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 :scheme pseudo-header field identifies a scheme that has a mandatory authority component (including "http" and "https"), the request MUST contain either an :authority pseudo-header field or a Host header 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_NAME variable 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 with SCRIPT_NAME and PATH_INFO, these keys can be used to complete the URL. Note, however, that HTTP_HOST, if present, should be used in preference to SERVER_NAME for reconstructing the request URL. SERVER_NAME and SERVER_PORT MUST 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:

$http_host:

$request:

$request_port (since nginx 1.29.3):

$server_name:

References

HTTP specifications:

CGI and associated:

NGINX documentation:

About this vulnerability:

Related security research:


  1. 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_pass directive, in this case backend.example) when used as a reverse proxy. The proxy_set_header Host $http_host; directive attempts to pass the authority of the incoming HTTP request. ↩︎