Opened 9 months ago

Closed 9 months ago

Last modified 9 months ago

#2329 closed defect (wontfix)

Unexpected request routing when Host header value contains colon

Reported by: Pēteris Caune Owned by:
Priority: minor Milestone:
Component: nginx-core Version: 1.18.x
Keywords: Cc:
uname -a: Linux ubuntu-impish 5.13.0-28-generic #31-Ubuntu SMP Thu Jan 13 17:41:06 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux
nginx -V: nginx version: nginx/1.18.0 (Ubuntu)
built with OpenSSL 1.1.1l 24 Aug 2021
TLS SNI support enabled
configure arguments: --with-cc-opt='-g -O2 -ffile-prefix-map=/build/nginx-wlVHrx/nginx-1.18.0=. -flto=auto -ffat-lto-objects -fstack-protector-strong -Wformat -Werror=format-security -fPIC -Wdate-time -D_FORTIFY_SOURCE=2' --with-ld-opt='-Wl,-Bsymbolic-functions -flto=auto -Wl,-z,relro -Wl,-z,now -fPIC' --prefix=/usr/share/nginx --conf-path=/etc/nginx/nginx.conf --http-log-path=/var/log/nginx/access.log --error-log-path=/var/log/nginx/error.log --lock-path=/var/lock/nginx.lock --pid-path=/run/nginx.pid --modules-path=/usr/lib/nginx/modules --http-client-body-temp-path=/var/lib/nginx/body --http-fastcgi-temp-path=/var/lib/nginx/fastcgi --http-proxy-temp-path=/var/lib/nginx/proxy --http-scgi-temp-path=/var/lib/nginx/scgi --http-uwsgi-temp-path=/var/lib/nginx/uwsgi --with-compat --with-debug --with-pcre-jit --with-http_ssl_module --with-http_stub_status_module --with-http_realip_module --with-http_auth_request_module --with-http_v2_module --with-http_dav_module --with-http_slice_module --with-threads --add-dynamic-module=/build/nginx-wlVHrx/nginx-1.18.0/debian/modules/http-geoip2 --with-http_addition_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_sub_module

Description

Here's my nginx.conf:

user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
	worker_connections 768;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    error_log /var/log/nginx/error.log;

    server {
        listen 80;

        server_name foo.com;

        location / {
            default_type text/plain;
            return 200 "hello from foo.com";
        }

        error_page 500 502 503 504 /500.html;
    }

    server {
        listen 80 default_server;
        server_name _;

        location / {
            return 403 "sorry";
        }
    }
}

I'm expecting the server to return "sorry" if the "Host" header is anything but "foo.com".

Somebody's apparently running Burp Suite on my server, and I noticed and interesting behavior when they send a "Host: foo.com:more-stuff-here" header: NGINX routes the request to the first "server" section. It looks as if it ignores the colon and everything after it in the header value.

I can reproduce it locally with the above nginx.conf:

$ curl -H "Host: foo.com" http://127.0.0.1
hello from foo.com

$ curl -H "Host: foo.com:z" http://127.0.0.1
hello from foo.com

$ curl -H "Host: foo.comz" http://127.0.0.1
sorry

Why does NGINX do this? Is this an expected behavior? What should I change in nginx.conf to ensure requests with "Host: foo.com:more-stuff-here" header go to the default block?

Change History (3)

comment:1 by Maxim Dounin, 9 months ago

Resolution: wontfix
Status: newclosed

Anything after the : is considered to be the optional TCP port (see RFC 7230, section "5.4. Host") and ignored by nginx, as server_name is only used to match names, and not ports.

Note that accepting only Host: foo.com is incorrect, as at least Host: foo.com:80 is perfectly valid for your particular configuration and can be used by legitimate clients. Further, other port numbers might be valid as well, depending on the TCP port forwarding being used.

If you nevertheless want to additionally limit requests accepted, you can do so with additional checking of the $http_host variable with the rewrite module. Note though that there can be requests without the Host header at all, assuming the host name is provided in the request line, following the absolute form of the request line (or without the Host header at all in HTTP/1.0, but these will end up in the default server block).

In theory, the code can be changed to perform stricter syntax checking and only allow digits and not arbitrary characters in the port, it is not clear why it can be needed/useful (as syntactically valid port numbers have to be accepted anyway, see above). If you think this can be beneficial, please elaborate why do you think so.

comment:2 by Pēteris Caune, 9 months ago

If you think this can be beneficial, please elaborate why do you think so.

I'll add a little context about my particular situation. I'm using NGINX as a reverse proxy in front of a Django application. In production mode (DEBUG=False), Django checks the "Host" header against an allowlist (the ALLOWED_HOSTS setting). It matches the hostname only, and ignores the port part. *But*, it looks like it still requires the port to be a numeric value. So, with ALLOWED_HOSTS=["foo.com"], Django will accept Host: foo.com:123 and Host: foo.com:456 but it will reject Host: foo.com:abc.

When a client makes a request with a "Host: foo.com:abc" header, NGINX passes the request to the Django app, and the Django app returns HTTP 400 (and logs a warning). This is fine, but my intuition was NGINX would act as a "shield" in front of the backend app, making sure only valid, well-formed requests go through to the backend app. And I expected a Host header value "foo.com:abc" would not be counted as valid.

ALLOWED_HOSTS documentation: https://docs.djangoproject.com/en/4.0/ref/settings/#allowed-hosts

comment:3 by Maxim Dounin, 9 months ago

Thanks for the details. In this particular case Django's syntax checking is more strict than nginx one, though it does not seem to be something important.

Note: See TracTickets for help on using tickets.