Django Best Practices: Security

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 that can be run on the production version of your website before deployment. As well as copious notes on security in the official documentation.

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--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 fundamental 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. 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, 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. 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 is a randomly generated string used for cryptographic 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 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 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:

# 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 automatically redirects requests over HTTP to HTTPS. It should be set to True.

HTTP Strict Transport Security (HSTS) is a security policy that lets our server enforce that web browsers should only interact via HTTPS by adding a Strict-Transport-Security header. There are three implicit HSTS configurations in a settings.py file that need to be updated for production:

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. These 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 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

# 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.

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 walks through building out a production-ready website with proper security features, performance tips, Docker usage, and more.