106

After upgrading to Django 1.5, I started getting errors like this:

Traceback (most recent call last):

File "/usr/local/lib/python2.7/dist-packages/django/core/handlers/base.py", line 92, in get_response
response = middleware_method(request)

File "/usr/local/lib/python2.7/dist-packages/django/middleware/common.py", line 57, in process_request
host = request.get_host()

File "/usr/local/lib/python2.7/dist-packages/django/http/request.py", line 72, in get_host
"Invalid HTTP_HOST header (you may need to set ALLOWED_HOSTS): %s" % host)

SuspiciousOperation: Invalid HTTP_HOST header (you may need to set ALLOWED_HOSTS): www.google.com

<WSGIRequest
path:/,
GET:<QueryDict: {}>,
POST:<QueryDict: {}>,
COOKIES:{},
META:{'CONTENT_LENGTH': '',
'CONTENT_TYPE': '',
'DOCUMENT_ROOT': '/etc/nginx/html',
'HTTP_ACCEPT': 'text/html',
'HTTP_HOST': 'www.google.com',
'HTTP_PROXY_CONNECTION': 'close',
'HTTP_USER_AGENT': 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)',
'PATH_INFO': u'/',
'QUERY_STRING': '',
'REMOTE_ADDR': '210.245.91.104',
'REMOTE_PORT': '49347',
'REQUEST_METHOD': 'GET',
'REQUEST_URI': '/',
u'SCRIPT_NAME': u'',
'SERVER_NAME': 'www.derekkwok.net',
'SERVER_PORT': '80',
'SERVER_PROTOCOL': 'HTTP/1.0',
'uwsgi.node': 'derekkwok',
'uwsgi.version': '1.4.4',
'wsgi.errors': <open file 'wsgi_errors', mode 'w' at 0xb6d99c28>,
'wsgi.file_wrapper': <built-in function uwsgi_sendfile>,
'wsgi.input': <uwsgi._Input object at 0x953e698>,
'wsgi.multiprocess': True,
'wsgi.multithread': False,
'wsgi.run_once': False,
'wsgi.url_scheme': 'http',
'wsgi.version': (1, 0)}>

I've set ALLOWED_HOSTS = ['.derekkwok.net'] in my settings.py file.

What is going on here? It someone pretending to be Google and accessing my site? Or is it a benign case of someone setting their HTTP_HOST header incorrectly?

Derek Kwok
  • 12,768
  • 6
  • 38
  • 58
  • Did you figure out how to fix this? Facing the same problem. Logging about a hundred of these errors every day. No idea if it's something I need to worry about. – blinduck May 08 '13 at 03:29
  • 3
    This blog post provides a nice way to stop the emails: http://www.tiwoc.de/blog/2013/03/django-prevent-email-notification-on-suspiciousoperation/ – Derek Kwok May 08 '13 at 05:03
  • 2
    Alternate answers http://stackoverflow.com/a/25114003/1714030, http://stackoverflow.com/a/21170400/1714030, http://stackoverflow.com/q/15238506/1714030 and http://stackoverflow.com/q/18220519/1714030 – Daniel Backman Aug 04 '14 at 07:51

4 Answers4

154

If you're using Nginx to forward requests to Django running on Gunicorn/Apache/uWSGI, you can use the following to block bad requests. Thanks to @PaulM for the suggestion.

upstream app_server {
    server unix:/tmp/gunicorn_mydomain.example.sock fail_timeout=0;
}

server {

    ...

    ## Deny illegal Host headers
    if ($host !~* ^(mydomain.example|www.mydomain.example)$ ) {
        return 444;
    }

    location  / {
        proxy_pass               http://app_server;
        ...
    }

}
Stephen Ostermiller
  • 23,933
  • 14
  • 88
  • 109
Brent O'Connor
  • 5,692
  • 7
  • 23
  • 27
  • 8
    It would be excellent to see this as an improvement to the docs *hint hint* :) – Paul McMillan Jul 06 '13 at 17:34
  • For some reason this didn't work for me... However, @tiwoc proposed a Django-level workaround that worked like a charm. See [my answer](http://stackoverflow.com/a/19534738/209050). – mgalgs Oct 23 '13 at 06:52
  • Yes, I believe you :). However I personally prefer the django solution since it lives with my project and provides more flexibility if I really want it. – mgalgs Oct 23 '13 at 16:11
  • You're adding extra load to your servers. Instead of Nginx telling the bots to go away, you're letting the bots hit your Django app. – Brent O'Connor Oct 23 '13 at 19:39
  • 1
    @webjunkie, From your link, "There are cases where you simply cannot avoid using an if, for example if you need to test a variable which has no equivalent directive." My example uses it correctly and works well in my production environment. So in conclusion, DO do it like this! :) – Brent O'Connor Oct 30 '13 at 18:55
  • 2
    Well you can easily avoid it: Just specify only the server_name you need, and let the rest be handled by a default server handler. – webjunkie Oct 31 '13 at 10:08
  • @webjunkie, By all means, test it and then post an example. – Brent O'Connor Oct 31 '13 at 15:20
  • 1
    See this answer for a similar Apache configuration: http://stackoverflow.com/a/18792080/ – Denilson Sá Maia Jun 20 '14 at 19:17
  • 1
    From the link provided by webjunkie: "Directive if has problems when used in location context". The example given by Brent uses the `if` inside the `server` block and not in the `location` block. Does that mean that the `if` is okay in this instance? – brian buck Mar 18 '15 at 14:44
  • [Here](http://mgalgs.github.io/2016/07/06/block-invalid-http-hosts-with-haproxy-and-django.html)'s an example for `haproxy`. – mgalgs Jul 07 '16 at 06:50
  • 1
    I used the answer below from @webjunkie with no `if`. It works well. – Gezim Jun 19 '17 at 17:58
  • If someone is probing my site for vulnerability, shouldn't I know? – mehmet May 24 '18 at 15:49
  • Thank you for this solution @Brent. Does it affect performances due to the check of if statement for every single request ? – Quentin Jan 26 '19 at 17:20
  • @Quentin, I haven't done any benchmarking or noticed any performance issues. – Brent O'Connor Jan 26 '19 at 21:26
  • unfortunately only that does not solve the problem but it's useful – Fernando Valente Apr 29 '21 at 19:19
  • When using something like this, is there a preferred way to allow health checks for individual servers that are under load balancers? Health checkers will likely not be passing host headers. – Michael Tom Mar 07 '23 at 19:10
66

If your ALLOWED_HOSTS is set correctly, then it is possible someone is probing your site for the vulnerability by spoofing the header.

There is discussion right now by the Django developers to change this from a 500 internal server error to a 400 response. See this ticket.

Henrik Heimbuerger
  • 9,924
  • 6
  • 56
  • 69
Brian Neal
  • 31,821
  • 7
  • 55
  • 59
  • 1
    I think a more likely explanation is web crawlers (robots) simply crawling public IP addresses on port 80 - in which case you would want to allow them. – markmnl May 16 '14 at 04:12
  • 18
    @markmnl A legitimate web crawler should not be forging host headers. – Brian Neal May 16 '14 at 14:43
  • 1
    It is just connecting using the IP address not the domain name and the IP address is not in the ALLOWED_HOSTS - or at least that is what was happening with me - I could repro it by point my browser to the IP address. – markmnl May 17 '14 at 02:38
  • Yep. And in any half busy site, this happens all day every day. They've fixed it now, but here's a "drop-in" app that sorts it out across all versions along with an error rate filter. https://github.com/litchfield/django-safelogging – s29 Jun 18 '14 at 01:21
  • After deploying my website on the internet. I found a lot of people are trying to get access to my website using invalid host. Not only using IP address. I think this may be some people trying to find a website which cannot defend a csrf attack. – ramwin Jan 19 '18 at 10:28
31

When using Nginx you could set up you servers in a way only requests to the hosts you want get to Django in the first place. That should give you no SuspiciousOperation errors anymore.

server {
    # default server

    listen 80;
    server_name _ default;

    return 444;
}
server {
    # redirects

    listen 80;
    server_name example.com old.stuff.example.com;

    return 301 http://www.example.com$request_uri;
}
server {
    # app

    listen 80;
    server_name www.example.com; # only hosts in ALLOWED_HOSTS here

    location  / {
        # ...
    }
    # ... your config/proxy stuff
}
webjunkie
  • 6,891
  • 7
  • 46
  • 43
  • 2
    I like this approach over using the `if` approach suggested by Brent, but I can't get it to work with port 443. I tried mimicking your suggestion (with the listen port changed), and my actual SSL site doesn't load -- it gets captured by this entry I added. Any ideas on how to fix? – Dolan Antenucci Nov 16 '14 at 19:31
  • 1
    Another poster on [ServerFault.com](http://serverfault.com/a/593668/58892) had similar issues, so I followed his recommendation on the if-statement approach for 443 traffic only – Dolan Antenucci Nov 16 '14 at 20:01
  • 2
    Seems that you have to specify the path to certificate files if you want to catch SSL requests too (even though you just want to discard): `server { listen 80 default_server; listen 443; server_name _; ssl_certificate /path/to/file.crt; ssl_certificate_key /path/to/file.key; return 444; }` – n__o Dec 29 '14 at 20:20
  • What will Nginx return if request's HOST is invalid? 50x or 40x? – laike9m Oct 07 '15 at 04:21
  • What's the extra in this configuration? I have the server name set both in the redirects and the app section, I still get `Invalid HTTP_HOST header` (with Django 1.8.x) – Csaba Toth Jan 02 '18 at 18:32
16

This is fixed in newer versions of Django, but if you're using an affected version (e.g. 1.5) you can add a filter to your logger handler to get rid of these, as outlined in this blog post.

Spoiler:

from django.core.exceptions import SuspiciousOperation

def skip_suspicious_operations(record):
  if record.exc_info:
    exc_value = record.exc_info[1]
    if isinstance(exc_value, SuspiciousOperation):
      return False
  return True

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'filters': {
        'require_debug_false': {
            '()': 'django.utils.log.RequireDebugFalse',
        },
        # Define filter
        'skip_suspicious_operations': {
            '()': 'django.utils.log.CallbackFilter',
            'callback': skip_suspicious_operations,
        },
    },
    'handlers': {
        'mail_admins': {
            'level': 'ERROR',
            # Add filter to list of filters
            'filters': ['require_debug_false', 'skip_suspicious_operations'],
            'class': 'django.utils.log.AdminEmailHandler'
        }
    },
    'loggers': {
        'django.request': {
            'handlers': ['mail_admins'],
            'level': 'ERROR',
            'propagate': True,
        },
    }
}
mgalgs
  • 15,671
  • 11
  • 61
  • 74