69

Ever since I deployed a site running Django 1.7 alpha (checked out from Git), I've been occasionally receiving error messages with titles like:

"Invalid HTTP_HOST header: 'xxx.xxx.com'"

I realize that this is due to the Host: HTTP header being set to a hostname not listed in ALLOWED_HOSTS. However, I have no control over when and how often someone sends a request to the server with a forged hostname. Therefore I do not need a bunch of error emails letting me know that someone else is attempting to do something fishy.

Is there any way to disable this error message? The logging settings for the project look like this:

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'filters': {
        'require_debug_false': {
            '()': 'django.utils.log.RequireDebugFalse'
        }
    },
    'handlers': {
        'mail_admins': {
            'level': 'ERROR',
            'filters': ['require_debug_false'],
            'class': 'django.utils.log.AdminEmailHandler'
        }
    },
    'loggers': {
        'django.request': {
            'handlers': ['mail_admins'],
            'level': 'ERROR',
            'propagate': True,
        },
    }
}
Nathan Osman
  • 71,149
  • 71
  • 256
  • 361
  • 1
    Why are you using an alpha version in production? – Burhan Khalid Sep 13 '13 at 16:52
  • 1
    @Burhan: because a couple of important bugs were fixed after the latest stable release. – Nathan Osman Sep 13 '13 at 16:57
  • 3
    How are you hosting it (e.g. WSGI through Apache)? I'd be a big fan of trying to figure out how to block this before it hit Django – Foon Sep 13 '13 at 17:00
  • @Foon: Apache via mod_wsgi. – Nathan Osman Sep 13 '13 at 17:01
  • I agree with @Foon. You shouldn't be ignoring this error. You should be blocking requests with invalid headers before they reach Django. – Mark Lavin Sep 13 '13 at 17:28
  • @Mark: oh now I see what you're saying. Apache shouldn't be passing the request along with the wrong hostname. – Nathan Osman Sep 13 '13 at 17:31
  • I think the best way to work with http requests is middleware... try to catch it there – oleg.foreigner Sep 17 '13 at 10:19
  • 4
    Does anyone know the name for this attack or why it would ever matter if someone manages to query your site with a hostname that you did not intend? I mean, because they are doing it, I assume there has to be some vulnerability, but it's not obvious to me what it could be. – kloddant Aug 08 '18 at 14:25

13 Answers13

66

You shouldn't be ignoring this error. Instead you should be denying the request before it reaches your Django backend. To deny requests with no HOST set you can use

SetEnvIfNoCase Host .+ VALID_HOST
Order Deny,Allow
Deny from All
Allow from env=VALID_HOST

or force the match to a particular domain (example.com)

SetEnvIfNoCase Host example\.com VALID_HOST
Order Deny,Allow
Deny from All
Allow from env=VALID_HOST
Mark Lavin
  • 24,664
  • 5
  • 76
  • 70
  • 39
    That is, you're using Apache only. Of you use nginx to forward requests, refer to this answer: http://stackoverflow.com/a/17477436/188614 – Diego Ponciano Sep 20 '13 at 12:31
  • 1
    Mark -what do I need to enable to get this behavior you show above? also what config file would this go into if I'm using traditional apache 2.x ? – Toran Billups Jan 18 '14 at 20:36
  • You need `mod_setenvif` enabled. This would go either in `httpd.conf` or where your virtual host for the domain is defined. – Mark Lavin Jan 18 '14 at 23:45
  • The information about how to do this in the Apache configuration is useful to me. But I am missing information about why it is I must do this in the Apache configuration. Why can't django be trusted to deny the requests? – kasperd Sep 07 '14 at 08:22
  • @kasperd Django will deny the request but depending on your logging setting you may get an email each time. This is what the question wanted to avoid. Denying the request earlier (Apache/Nginx/etc) is still recommended https://docs.djangoproject.com/en/1.7/topics/security/#host-header-validation – Mark Lavin Sep 07 '14 at 13:04
  • I have tested this approach, and there is one case that I simply cannot get to work correctly. Sending an HTTP/1.0 request with no host header is perfectly valid, and django can handle such a request without problems. Sending a request with a host header containing the empty string is however not accepted by django, and produce an error. So I am trying to distinguish those two cases because django can handle one and not the other. But I simply cannot get apache to distinguish them because referring an absent header field appears to always produce an empty string. – kasperd Sep 27 '14 at 23:43
  • Is an empty header field a real use case? In other words, do some crawlers or browsers not set this header? This question is not suggestive ... I really do not know. – Torsten Bronger Jan 05 '15 at 06:49
  • 1
    The header is required in HTTP 1.1 but you can't control what values the client will put in it. Validating the header isn't about handling well behaved clients, it's about handling misbehaved and potentially malicious clients. It isn't consistent how a blank host is handled `curl -v --header "Host: " http://example.com` returns a 400 and `curl -v --header "Host: " http://google.com` returns a 200. – Mark Lavin Jan 05 '15 at 13:50
  • 9
    Adding this code to my virtualhost file results in errors like, "Order not allowed here" or "Deny not allowed here." – tufelkinder Mar 30 '15 at 15:42
  • 5
    @tufelkinder : you can put the code in your `virtualhost` configuration inside a new ` ` directive. – rom Apr 15 '15 at 07:59
  • 6
    You should also provided where to exactly put this configuration, for someone who is new to Apache, this would be helpful. – Karan Kumar Feb 06 '17 at 07:16
  • It seems in Apache 2.4 we should use `Require` directives instead of `Order`, `Deny` and `Allow`: [Apache 2.4 docs](https://httpd.apache.org/docs/2.4/upgrading.html) – djvg Feb 08 '20 at 18:11
  • 1
    How do you modify this for a list of valid hosts, not just a single one? – kloddant Mar 18 '22 at 13:33
  • Can someone clarify where exactly this goes? Do we create a file in virtualenv/domain-example.com called httpd.conf? – squidg May 17 '23 at 11:35
42

You can add this to the loggers section of your logging configuration:

    'django.security.DisallowedHost': {
        'handlers': ['mail_admins'],
        'level': 'CRITICAL',
        'propagate': False,
    },

This sets the logging threshold to above the ERROR level that Django uses when a SuspiciousOperation is detected.

Alternatively, you can use e.g. a FileHandler to log these events without emailing them to you. For example, to use a dedicated file just for these specific events, you could add this to the handlers section:

    'spoof_logfile': {
        'level': 'ERROR',
        'class': 'logging.FileHandler',
        'filename': '/path/to/spoofed_requests.log',
    },

and then use this in the loggers section:

    'django.security.DisallowedHost': {
        'handlers': ['spoof_logfile'],
        'level': 'ERROR',
        'propagate': False,
    },

Note that the suggestion made in the Django docs, to use

    'django.security.DisallowedHost': {
        'handlers': ['null'],
        'propagate': False,
    },

depends on you running Python 2.7 or later - on 2.6, logging doesn't have a NullHandler.

Dan
  • 12,409
  • 3
  • 50
  • 87
Vinay Sajip
  • 95,872
  • 14
  • 179
  • 191
13

Using Apache 2.4, there's no need to use mod_setenvif. The HTTP_HOST is already a variable and can be evaluated directly:

WSGIScriptAlias / /path/to/wsgi.py

<Directory /path/to>
    <Files wsgi.py>
        Require expr %{HTTP_HOST} == "example.com"
    </Files>
</Directory>
liquidki
  • 1,234
  • 16
  • 16
12

Here's NGINX example that should prevent your django from receiving rubbish requests.

server {
    listen 80 default_server;
    listen [::]:80 default_server;
    listen 443 ssl default_server;
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # managed by Certbot
    return 444;
}


server {
    listen 80;
    # This will keep Django from receiving request with invalid host
    server_name <SERVER_IP> your.domain.com;
    ...
discover
  • 411
  • 1
  • 6
  • 16
Janusz Skonieczny
  • 17,642
  • 11
  • 55
  • 63
  • The official docs (https://nginx.org/en/docs/http/request_processing.html#how_to_prevent_undefined_server_names) recommend returning a 444 status code in this situation. – Laurent S May 22 '17 at 06:22
  • Also, it should be `default_server` not `default` (same link as above for reference), at least as of today :) – Laurent S May 22 '17 at 06:23
  • 2
    If using HTTPS, don't forget to repeat the same server block as above, except with port 443 and the ssl certificate specified. Just wasted an hour trying to figure out why I kept getting the error message. – MrP01 Aug 07 '20 at 16:20
  • Added HTTPS section in answer. – discover Sep 29 '21 at 06:53
  • you could add the ipv6 catching syntax for ssl connection as well – lapin Nov 18 '22 at 02:16
8

you could silence that particular SuspiciousOperation with something like

'loggers': {
    'django.security.DisallowedHost': {
        'handlers': ['null'],
        'propagate': False,
   },

see this for more reference https://docs.djangoproject.com/en/dev/topics/logging/#django-security

EDIT

you also need to add a 'null' handler:

'handlers': {
    'null': {
        'level': 'DEBUG',
        'class': 'logging.NullHandler',
    },
}

probably you only need to add this and modify the level of error (replacing DEBUG with 'ERROR').

as always refer to the the documentation for the complete syntax and semantic.

DRC
  • 4,898
  • 2
  • 21
  • 35
  • 1
    This doesn't seem to work. I get an error from mod_wsgi: `"ValueError: Unable to configure logger 'django.security.DisallowedHost': Unable to add handler 'null': 'null'"` – Nathan Osman Aug 23 '13 at 06:21
  • 1
    do you have an handler named 'null' as the documentation says? I'll modify my answer to point to an example. – DRC Aug 24 '13 at 15:46
  • It's still not working. Here is the exact logging configuration I am using: http://quickmediasolutions.com/pastebin/15/django-152-logging-configuration – Nathan Osman Sep 03 '13 at 05:04
8

The django docs address this specifically. They recommend putting this in your logging settings

LOGGING = {
    # ...
    "handlers": {
        # ...
        "null": {
            "class": "logging.NullHandler",
        },
    },
    "loggers": {
        # ...
        "django.security.DisallowedHost": {
            "handlers": ["null"],
            "propagate": False,
        },
    },
}

Additionally, if you are using Sentry, you need to add this to prevent Sentry from picking it up:

from sentry_sdk.integrations.logging import ignore_logger
ignore_logger("django.security.DisallowedHost")
Zags
  • 37,389
  • 14
  • 105
  • 140
6

Another way to block requests with an invalid Host header before it reaches Django is to use a default Apache config with a <VirtualHost> that does nothing but return a 404.

<VirtualHost *:80>
</VirtualHost>

If you define this as your first virtual host (e.g. in 000-default.conf) and then follow it with your 'real' <VirtualHost>, complete with a <ServerName> and any <ServerAlias> entries that you want to match, Apache will return a 404 for any requests with a Host header that does not match <ServerName> or one of your <ServerAlias> entries. The key it to make sure that the default, 404 <VirtualHost> is defined first, either by filename ('000') or the first entry in your config file.

I like this better than the popular solution above because it is very explicit and easy to extend.

Bob Barcklay
  • 1,584
  • 1
  • 15
  • 22
  • This doesn't return a 404 for me, but just gives access to everything in the default document root, listing files etc. Isn't there a specific 404 response directive needed to make it more secure? – MagicLAMP May 24 '18 at 21:52
4

I can't comment yet, but since Order Deny, Allow is deprecated, the way to do this in a virtual host with the current Require directive is:

<Directory /var/www/html/>
    SetEnvIfNoCase Host example\.com VALID_HOST
    Require env VALID_HOST
    Options
</Directory>
f71316
  • 252
  • 3
  • 13
4

The other answers on this page are correct if you're simply looking to hide or disable the warning. If you're intentionally allowing every hostname the special value of * can be used as the ALLOWED_HOSTS setting.

Note: This may introduce security vulnerabilities.

Django uses the Host header provided by the client to construct URLs in certain cases. While these values are sanitized to prevent Cross Site Scripting attacks, a fake Host value can be used for Cross-Site Request Forgery, cache poisoning attacks, and poisoning links in emails.

Because even seemingly-secure web server configurations are susceptible to fake Host headers, Django validates Host headers against the ALLOWED_HOSTS setting in the django.http.HttpRequest.get_host() method.

To prevent hostname checking entirely, add the following line to your settings.py:

ALLOWED_HOSTS = ['*']

Source: https://github.com/django/django/blob/33c365781abbcc1b21a31b31d95d344a174df0d5/django/http/request.py#L653-L668

def validate_host(host, allowed_hosts):
    """
    Validate the given host for this site.

    Check that the host looks valid and matches a host or host pattern in the
    given list of ``allowed_hosts``. Any pattern beginning with a period
    matches a domain and all its subdomains (e.g. ``.example.com`` matches
    ``example.com`` and any subdomain), ``*`` matches anything, and anything
    else must match exactly.

    Note: This function assumes that the given host is lowercased and has
    already had the port, if any, stripped off.

    Return ``True`` for a valid host, ``False`` otherwise.
    """
    return any(pattern == '*' or is_same_domain(host, pattern) for pattern in allowed_hosts)
Wolph
  • 78,177
  • 11
  • 137
  • 148
  • This answer should include a caveat that it introduces potential security vulnerabilities – Zach Jul 15 '20 at 16:20
  • 1
    @Zach I've added a warning including the official documentation about potential security issues – Wolph Jul 15 '20 at 20:26
1

for multiple valid hosts you can:

SetEnvIfNoCase Host example\.com VALID_HOST
SetEnvIfNoCase Host example2\.com VALID_HOST
SetEnvIfNoCase Host example3\.com VALID_HOST
Require env VALID_HOST
ashier
  • 11
  • 3
1

Here's NGINX block needed to prevent your django from receiving such requests.

server {
    listen 80 default_server;
    listen [::]:80 default_server;
    listen 443 ssl default_server;
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # managed by Certbot
    return 444;
}
discover
  • 411
  • 1
  • 6
  • 16
0

In setting.py set:

ALLOWED_HOSTS = ['yourweb.com']
Adriaan
  • 17,741
  • 7
  • 42
  • 75
  • 4
    This does not actually solve the issue presented by the OP. As the OP has stated: "However, I have no control over when and how often someone sends a request to the server with a forged hostname." The problem is that every time a forged hostname goes through, the OP gets notified in a cumbersome way (email). What the OP wants is to get notifications that are not so invasive. Editing `ALLOWED_HOSTS` is not the solution here. – Louis Nov 29 '16 at 13:21
0

Also to handle the below error through Apache:

  • Invalid HTTP_HOST header: '_my.domain.com'. The domain name provided is not valid according to RFC 1034/1035.

We can use regex, ^[^_]+ would match a string of 1 or more character containing any character except underscore in the subdomain as in the below case.

We can apply it to wsgi.py file

<VirtualHost xxx.xxx.xxx.xxx:XX>
    ...

    SetEnvIfNoCase Host "^[^_]+\.my-domain\.com" VALID_HOST
    <Files wsgi.py>
        <RequireAll>
            Require all granted
            Require env VALID_HOST
        </RequireAll>
    </Files>
    
    ...
</VirtualHost>

With Require expr:

<VirtualHost xxx.xxx.xxx.xxx:XX>
    ...


    <Files wsgi.py>

        Require expr %{HTTP_HOST} =~ m#^[^_]+\.my-domain\.com#

    </Files>


    ...

</VirtualHost>

Or we can use <Location "/">, which is an easy way to apply a configuration to the entire server.

<VirtualHost xxx.xxx.xxx.xxx:XX>
    ...

    SetEnvIfNoCase Host "^[^_]+\.my-domain\.com" VALID_HOST
    <Location />
        <RequireAll>
            Require all granted
            Require env VALID_HOST
        </RequireAll>
    </Location>
    
    ...
</VirtualHost>

From Apache docs:

When to use <Location "/">

Use <Location> to apply directives to content that lives outside the filesystem. For content that lives in the filesystem, use and . An exception is <Location "/">, which is an easy way to apply a configuration to the entire server.

The answer is based on Apache Module mod_setenvif and how to block persistent requests from a particular robot.

NKSM
  • 5,422
  • 4
  • 25
  • 38