51

Is there a way to set a range of ALLOWED_HOSTS IPs in django?

Something like this:

ALLOWED_HOSTS = ['172.17.*.*']
Alex T
  • 4,331
  • 3
  • 29
  • 47
  • 1
    I was about to answer "yes" but did some google digging and can't find a specific example where someone has done this. According to the [docs](https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts), you can use a lone wildcard in the list, like `ALLOWED_HOSTS = ['*']`, (which is not recommended for security reasons) but I've seen nothing like your example yet. I'm still leaning towards "yes" but I'll be unconvinced until I see a reference stating this explicitly. This may be a stupid question, but have you tried it to see if it will raise any errors in django? – Ian May 04 '16 at 15:41
  • It would be better to offload this to a web server which were made to handle this efficiently. Or maybe even setup firewall rules, the higher in stack a traffic could be filtered the better. – serg May 04 '16 at 16:49
  • @Nez, yes I do. Look at my answer below. I found solution for this problem. – Alex T May 04 '16 at 18:32
  • Nice. I knew it could be done, but I didn't know what kind of solution I was looking for. Middleware makes sense. @serg 's suggestion that it should be handled higher in the stack is good but this should be okay for a relatively low-traffic build, right? – Ian May 04 '16 at 18:49
  • @serg's suggestion definitely suits for highload projects much more then middlware. But I was looking for django level solution. – Alex T May 04 '16 at 18:57

7 Answers7

60

No, this is not currently possible. According to the docs, the following syntax is supported:

['www.example.com']  # Fully qualified domain
['.example.com']  # Subdomain wildcard, matches example.com and www.example.com 
['*']  # Matches anything

If you look at the implementation of the validate_host method, you can see that using '*' by itself is allowed, but using * as a wildcard as part of a string (e.g. '172.17.*.*') is not supported.

Alasdair
  • 298,606
  • 55
  • 578
  • 516
46

I posted a ticket on Django however I was shown this could be achieved by doing the following

from socket import gethostname, gethostbyname 
ALLOWED_HOSTS = [ gethostname(), gethostbyname(gethostname()), ] 

Update. If you are using docker the following code is better as gethostbyname doesn't get the correct information.

from socket import gethostname, gethostbyname, gethostbyname_ex
ALLOWED_HOSTS = [ gethostname(), ] + list(set(gethostbyname_ex(gethostname())[2]))

The reason it gets converted to a set is that fact that gethostbyname_ex can return duplicates.

The link to the ticket on django website is.

https://code.djangoproject.com/ticket/27485

Thomas Turner
  • 2,722
  • 1
  • 27
  • 23
  • The fall creators update for Windows 10 broke this method for me, as `gethostname()` was returning the Hyper-V ethernet switch address instead. You can use `ALLOWED_HOSTS = [ gethostname(), ] + gethostbyname_ex(gethostname())[2]` instead. – adam b Nov 21 '17 at 14:26
  • This is really helpful, running Django on a k8s cluster, where the node is unknown until runtime. – Laizer May 27 '18 at 18:29
  • 1
    Isnt it unsafe? – Karl Zillner Aug 31 '18 at 20:34
  • @KarlZillner I can't see why this is unsafe – Thomas Turner Sep 03 '18 at 14:12
  • 1
    @KarlZillner no that not true. It only adds the ipaddress of your machine no domain name will work they will get a Invalid HTTP_HOST error. Where "*" will allow all ipaddress and all domains – Thomas Turner Sep 04 '18 at 18:52
  • 2
    Unfortunately this does only allow to add the machine's ip and hostname, not any other server we trust but don't know a static ip address for, like an Amazon load balancer for example. If adding a wildcard was possible I could allow for subnet range, but now I had to work around the problem using nginx to rewrite the host. – Michel Rugenbrink Sep 27 '19 at 21:02
  • For those working with Kubernetes this answer needed a small addition to work for me: `ALLOWED_HOSTS = [ gethostname(), gethostbyname(gethostname()), '.example.com']` – Kurt Mar 16 '21 at 03:32
  • This looks good in theory but didn't work. – peterretief Jan 13 '22 at 07:31
  • @peterretief try from socket import gethostname, gethostbyname, gethostbyname_ex ALLOWED_HOSTS = [ gethostname(), ] + list(set(gethostbyname_ex(gethostname())[2])) – Thomas Turner Jan 13 '22 at 16:56
29

Mozilla have released a Python package called django-allow-cidr which is designed to solve exactly this problem.

The announcement blog post explains that it's useful for things like health checks that don't have a Host header and just use an IP address.

You would have to change your IP address '172.17.*.*' slightly to be a CIDR range like 172.17.0.0/16

alexmuller
  • 2,207
  • 3
  • 23
  • 36
  • 3
    Note that is would allow something malicious like `172.17.malicious.host.com` – Cole Aug 23 '19 at 20:10
  • This worked for me; was having the issue where my health checks were failing as the originating host was on the internal subnet. Adding `10.0.0.0/16` to `ALLOWED_CIDR_NETS` worked perfectly.`ALLOWED_CIDR_NETS = ['10.0.0.0/16']` – Goran Oct 31 '19 at 10:56
  • 2
    @Cole it will not. Looking at the source https://github.com/mozmeao/django-allow-cidr/blob/master/allow_cidr/middleware.py You can see that it will only match against the specific IP. `self.allowed_cidr_nets = [IPNetwork(net) for net in allowed_cidr_nets]` `for net in self.allowed_cidr_nets:` You can see more about IPNetwork https://netaddr.readthedocs.io/en/latest/tutorial_01.html#slicing under "Use of generators ensures working with large IP subnets is efficient." – sanchaz Jun 22 '21 at 03:56
26

Here is a quick and dirty solution.

ALLOWED_HOSTS += ['172.17.{}.{}'.format(i,j) for i in range(256) for j in range(256)]
Arbab Nazar
  • 22,378
  • 10
  • 76
  • 82
Ivandir
  • 355
  • 4
  • 3
  • 2
    `['172.17.{}.{}'.format(i,j) for i in range(256) for j in range(256)]` Same as above using format. Liking it. – Nomen Nescio Oct 16 '18 at 16:26
  • `['192.168.1.{}'.format(i) for i in range(256)]` Thanks, i just wanted to point out that you can also do this, if you know need only the last number like i did. – blobbymatt Feb 15 '19 at 09:14
  • 1
    Not a good idea for debugging, the dumped environments look dirty – Itachi Aug 12 '19 at 05:18
5

If we take a look into how Django validates hosts, we can gain insight into how we can make more flexible ALLOWED_HOSTS entries:

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)

. . .

def is_same_domain(host, pattern):
    """
    Return ``True`` if the host is either an exact match or a match
    to the wildcard pattern.

    Any pattern beginning with a period matches a domain and all of its
    subdomains. (e.g. ``.example.com`` matches ``example.com`` and
    ``foo.example.com``). Anything else is an exact string match.
    """
    if not pattern:
        return False

    pattern = pattern.lower()
    return (
        pattern[0] == '.' and (host.endswith(pattern) or host == pattern[1:]) or
        pattern == host
    )

Here is a RegexHost utility which can make it through this validation.

class RegexHost(str):
    def lower(self):
        return self

    def __init__(self, pattern):
        super().__init__()
        self.regex = re.compile(pattern)

    def __eq__(self, other):
        # override the equality operation to use regex matching
        # instead of str.__eq__(self, other) 
        return self.regex.match(other)

Which can be used like so:

# this matches '172.17.*.*' and also many impossible IPs
host = RegexHost(r'172\.17\.[0-9]{1,3}\.[0-9]{1,3}')

# Un-comment the below assertions to prove to yourself that this host
# validation works. Do not leave these assertions active in 
# production code for startup performance considerations.

# assert all(host == f'172.17.{i}.{j}' for i in range(256) for j in range(256))
# assert not any(host == f'172.18.{i}.{j}' for i in range(256) for j in range(256))
ALLOWED_HOSTS = [host]
DragonBobZ
  • 2,194
  • 18
  • 31
  • 1
    are you suggesting a 65K+ items array just to validate the host? – Ramy M. Mousa Jan 13 '22 at 17:18
  • 1
    No? I would expect someone to see these assertions, acknowledge that they pass, and remove them from the settings file (if that's where they were testing). – DragonBobZ Jan 13 '22 at 22:11
  • 3
    Keep in mind that this will only run at server startup, so even if they did leave it in their settings file, the performance of the app would be otherwise unaffected. I'll comment them out just in case someone was confused by this. – DragonBobZ Jan 13 '22 at 22:25
3

I've found such solution for filtering range of IPs:

https://stackoverflow.com/a/36222755/3766751

Using this approach we can filter IPs by any means (f.e. with regex).

from django.http import HttpResponseForbidden

class FilterHostMiddleware(object):

    def process_request(self, request):

        allowed_hosts = ['127.0.0.1', 'localhost']  # specify complete host names here
        host = request.META.get('HTTP_HOST')

        if host[len(host)-10:] == 'dyndns.org':  # if the host ends with dyndns.org then add to the allowed hosts
            allowed_hosts.append(host)
        elif host[:7] == '192.168':  # if the host starts with 192.168 then add to the allowed hosts
            allowed_hosts.append(host)

        if host not in allowed_hosts:
            raise HttpResponseForbidden

        return None

Thanks for @Zorgmorduk

Community
  • 1
  • 1
Alex T
  • 4,331
  • 3
  • 29
  • 47
1

This solution works for me:

  • Add django-allow-cidr==0.5.0 to your requirements.txt
  • Add to your setting.py:

MIDDLEWARE = [ 'allow_cidr.middleware.AllowCIDRMiddleware', ... ]

ALLOWED_CIDR_NETS = ['172.17.0.0/16']

Like this:

ALLOWED_HOSTS = ['127.0.0.1', 'localhost', '.domain.com']
ALLOWED_CIDR_NETS = ['172.17.0.0/16']

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

MIDDLEWARE = [
    'allow_cidr.middleware.AllowCIDRMiddleware',
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
Miguel Conde
  • 813
  • 10
  • 22