Django Best Practices: Security
Tags: django, python, tutorial, beginners
Published: 2020-09-22 17:18:17

Django is a mature, battle-tested web framework with a well deserved reputation for security over the past 15+ years.However the internet remains a dangerous place and web security is an evolving field. Like most web frameworks, Django defaults to local development settings when a new project is created. The onus is on the developer to effectively manage both local defaults **and** customized production settings.

Django comes with an excellent [deployment checklist](https://docs.djangoproject.com/en/dev/howto/deployment/checklist/) that can be run on the production version of your website before deployment. As well as copious notes on [security in the official documentation](https://docs.djangoproject.com/en/dev/topics/security/).

This tutorial covers Django security best practices starting with the most important and working our way down the list.


## Django Version
The number one security recommendation is to always be on the latest version of Django. Django has a new major release every 9 months or so (2.2, 3.0, 3.1, etc) and a minor release with security/bug fixes almost monthly (3.1.1, 3.1.2, etc). Take the time to update regularly to the latest version--there is an official guide in the documentation on [upgrading to a newer version](https://docs.djangoproject.com/en/dev/howto/upgrade-version/)--run your test suite, and carry on with your Django project. 

Note that writing comprehensive tests and implementing CI so that the tests are run on each commit is a topic for another tutorial or full-length book.

## Environment Variables
There is a fundmental tension between settings intended for local development and production. Local development prizes speed, robust bug reports, and access to all features. Production requires locking down the website and minimizing access as much as possible. By default, new Django projects created with the `startproject` command have local development settings. The developer is responsible for updating them to production settings.

These days, the standard way to switch between local and production settings is with [environment variables](https://en.wikipedia.org/wiki/Environment_variable). Rather than having multiple `settings.py` files, a single `settings.py` file can be used with variables loaded in depending upon the environment: local or production.

There are multiple ways to implement environment variables and various third-party packages that can help. Personally, I like using [environs](https://github.com/sloria/environs), which has a Django-specific option that installs a number of additional packages that help with configuration.

## DEBUG 
Near the top of any `settings.py` file is the configuration for [DEBUG](https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-DEBUG). By default, it is set to `True` which generates, among other things, a rich and detailed error page that includes a traceback and lots of metadata including all currently defined Django settings. That is a roadmap to debugging locally, but also a guide for any hacker who wants to compromise your website if released in production. Therefore, make sure that in production `DEBUG` is set to `False`! 

## SECRET_KEY
The [SECRET_KEY](https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-SECRET_KEY) is a randomly generated string used for [cryptographic signing](https://docs.djangoproject.com/en/dev/topics/signing/) created whenever the `startproject` command is run. It is very important that `SECRET_KEY` actually be kept, well, secret.

All it takes is one Git commit and your `SECRET_KEY` is visible to anyone with access to your source code. In practice, most developers develop a working prototype of a website before adding environment variables and production configurations. Therefore, before the first deployment, take the time to generate a new `SECRET_KEY`. One approach is to use Python's built-in [secrets](https://docs.python.org/3/library/secrets.html) module.

```
$ python -c 'import secrets; print(secrets.token_urlsafe(38))'
```

The parameter `token_urlsafe` returns the number of bytes in a URL-safe text string. With Base64 encoding on average each byte has 1.3 characters. So using `38` results in 51 characters in this case. The important thing is that your `SECRET_KEY` has at least 50 characters. Each time you run the command, a new value is outputted.

## ALLOWED_HOSTS
The [ALLOWED_HOSTS](https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts) setting lists which hosts/domain names your Django site can serve. By default, it is set to the empty list, `[]`, aka any host or domain has access to your site. This needs to be changed in production to avoid HTTP Header attacks.

For local development `localhost:8000` and `127.0.0.1:8000` are commonly used in Django, therefore both should be explicitly added to the setting. And once you have a custom URL for your production location--for example, if you're using Heroku it will contain `.herokuapp.com`, this should be added as well.

A common `ALLOWED_HOSTS` setting for a Heroku app is therefore the following:

```python
# settings.py
ALLOWED_HOSTS = ['.herokuapp.com', 'localhost', '127.0.0.1'] 
```

## HTTPS/SSL
These days there is no reason **not** to deploy your entire website behind HTTPS. Otherwise a hacker could sniff authentication credentials, API keys, or really any information transferred between the client and server. There are a number of settings to enable full SSL support in a Django project. 

[SECURE_SSL_REDIRECT](https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-SECURE_SSL_REDIRECT) automatically redirects requests over HTTP to HTTPS. It should be set to `True`.

[HTTP Strict Transport Security (HSTS)](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) is a security policy that lets our server enforce that web browsers should only interact via HTTPS by adding a [Strict-Transport-Security header](https://docs.djangoproject.com/en/3.1/ref/middleware/#http-strict-transport-security). There are three implicit HSTS configurations in a `settings.py` file that need to be updated for production:

* [SECURE_HSTS_SECONDS](https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-SECURE_HSTS_SECONDS) = `0`
* [SECURE_HSTS_INCLUDE_SUBDOMAINS](https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-include-subdomains) = `False`
* [SECURE_HSTS_PRELOAD](https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-preload) = `False`

The `SECURE_HSTS_SECONDS` setting is set to `0` by default but the greater the better for security purposes. A good default is to set it to one month, 2,592,000 seconds, instead. `SECURE_HSTS_INCLUDE_SUBDOMAINS` forces subdomains to also exclusively use SSL so it should be set to `True` in production. And `SECURE_HSTS_PRELOAD` only has an effect when there is a non-zero value for `SECURE_HSTS_SECONDS`, but since we just set one, it will need to be set to `True`.

## Media Files
Most Django websites will contain files that are static and unchanging such as CSS, JavaScript, images, and so on. There are broadly referred to as `Static` files. For certain websites, user-uploaded content is a key feature, such as an image sharing website similar to Instagram. Django refers to user-uploaded content as `Media` and additional precautions must be made.

In general, you can't trust your users. Especially when they have the ability to upload something to your website. Therefore for websites with media files, hosting all static content on a cloud service or CDN, like Amazon's S3, is necessary. The third party package [django-storages](https://django-storages.readthedocs.io/en/latest/) is a popular way to configure this for your website and alleviate media security concerns.

## Admin
The Django admin app is a powerful built-in feature. However, since every Django project has it located at `/admin` by default, it is easy for a hacker to try to force their way into any Django website at this URL. An easy way to secure your admin is to simply change the URL for the admin. In the example below it has been changed to `anything-but-admin`

```python
# config/urls.py
urlpatterns = [
    path('anything-but-admin/', admin.site.urls), 
```

For additional tips on hardening your admin, see the article [10 Tips for Making the Django Admin More Secure](https://opensource.com/article/18/1/10-tips-making-django-admin-more-secure).

## Conclusion
There are many more security improvements that can be added to a Django project but this hits the highlights. Make sure to run the Django deployment checklist on your production settings to be sure you have made all the requisite changes.

```
$ python manage.py check --deploy
```

For further help on security, the book [Django for Professionals](https://djangoforprofessionals.com) walks through building out a production-ready website with proper security features, performance tips, Docker usage, and more.