Skip to content

rasoolsomji/django-security

Repository files navigation

Security tips for your Django project

Preamble

This is a collection of security vulnerabilities and possible mitigations that you can employ in your Django project. I've included a key below to differentiate between those that pose a likely and important threat, and those which you can get away with ignoring until your project matures.

There are code snippets, linked to from the relevant vulnerability, which you might find useful, they are not installable Django apps.

Most of the suggested solutions cover versions ≈1.8-2.2, but I have included specific version numbers where applicable with each solution.

Key:

- Low importance. An unlikely or inefficient attack surface, but which might need to be addressed to complete a security audit.

- Medium importance. While not mandatory, a production-grade Django project should consider mitigating these vulnerabilities. These may become high importance in the future as web standards evolve to penalise sites without these features.

Icons taken from icons8.com

  1. HTTP Headers
    1. HTTP Strict Transport Security (HSTS)
    2. Content Security Policy (CSP)
    3. X-Content-Type-Options
  2. Cookies
    1. Add SameSite attribute
    2. Add Secure attribute
    3. Rename Django defaults
    4. CSRF Settings
    5. Disuse 'expires' attribute
  3. User Management
    1. Username enumeration
    2. Forgot password limit
    3. Incorrect password limit
    4. Require strong passwords
    5. Transferable sessions
    6. Concurrent logons
    7. Require password change
  4. Webserver Settings
    1. Rate limit
    2. Disable support for old TLS versions
    3. Disable support for old TLS ciphers
  5. Admin
    1. Don't use /admin/
    2. Updating insecure version of jQuery

HTTP Headers

HTTP Strict Transport Security (HSTS)

Vulnerabilities:

SSL-stripping, man-in-the-middle

One-liner:

Forces browsers to redirect non-HTTP traffic to HTTPS

Further detail:

OWASP Cheatsheet

Implementation:

Django >= 1.8 allows you set the setting SECURE_HSTS_SECONDS (and SECURE_HSTS_INCLUDE_SUBDOMAINS etc)

Alternatively you can add the following line to your server block in your nginx configuration:

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

Things to note

  • If you includeSubDomains / SECURE_HSTS_INCLUDE_SUBDOMAINS, it may break other site functionality. For example, if you use SendGrid for sending emails with click tracking links, it does not work with HTTPs without further configuration
  • If you use the nginx add_header method, make sure it covers all relevant location blocks, such as your static files or user-uploaded files. You may need to add that add_header directive within your location /static/ or location /uploads/ blocks

Back to top

Content Security Policy (CSP)

Vulnerabilities:

XSS

One-liner:

You whitelist valid sources of executable scripts.

Further-detail:

Mozilla documentation

Implementation:

Django does not support this out of the box, so you need to either use a 3rd-party library, or you can use a <meta http-equiv="Content-Security-Policy"> tag within your HTML.

Things to note

  • A (good) CSP-policy will break all inline scripts and styles! So make sure you are only using external stylesheets and javascript files.
  • You need to include external sources (such as script files from CDNs)
  • You have the ability to test your policy before it takes effect

Back to top

X-Content-Type-Options

Vulnerabilities:

XSS

One-liner:

Preventing the execution of malicious files.

Further detail:

Mozilla documentation

Implementation:

Django >= 1.8 allows you set the setting SECURE_CONTENT_TYPE_NOSNIFF which you ought to set to True.

Starting from Django 3.0, SECURE_CONTENT_TYPE_NOSNIFF defaults to True.

Alternatively you can add the following line to your server block in your nginx configuration:

add_header X-Content-Type-Options "nosniff";

Back to top

Cookies

Add SameSite attribute

Vulnerabilities:

CSRF attack, information exposure

One-liner:

Adding this attribute prevents sending cookies (like Django's session id) when requesting resources (eg. images, fonts, scripts) hosted elsewhere.

Further Detail:

OWASP

Implementation:

Django 2.1 introduced CSRF_COOKIE_SAMESITE and SESSION_COOKIE_SAMESITE. Previous versions may make use of custom middleware - an example, tested in v2.0 can be found here - or by intervening at the webserver level. An albeit crude addition to an Apache config may look like:

Header edit Set-Cookie ^(.*)$ $1;Samesite=Lax

Things to note:

  • Django 2.1 sets the default SameSite value to 'lax' which is a sensible default, consider before changing its value to 'strict'
  • Modifying the CSRF cookie is redundant if you put the CSRF cookie in the session cookie

Back to top

Add Secure attribute

Vulnerabilities:

Session hijacking, man-in-the-middle

One-liner:

Adding this attribute only allows the transmission of cookies over https, if an attacker manages to get a user to use http, they will not be able to read the cookies.

Further Detail:

Pivotpoint blog

Implementation:

Django 1.4 introduced CSRF_COOKIE_SECURE and Django 1.7 SESSION_COOKIE_SECURE. Previous versions may make use of custom middleware or by intervening at the webserver level. An albeit crude addition to an Apache config may look like:

Header edit Set-Cookie ^(.*)$ $1;Secure

Things to note:

  • Your development server runs on http so if you want to transmit these cookies while testing locally, you should should disable this attribute. Something simple like only setting them to True if DEBUG == True in your settings.py works.
  • Modifying the CSRF cookie is redundant if you put the CSRF cookie in the session cookie

Back to top

Rename Django defaults

Vulnabilities:

Information exposure

One liner:

The Django default names for cookies mean than an attacker knows to probe Django-specific weaknesses

Further Detail:

CWE

Implementation:

Since at least Django 1.4, you can edit the setting SESSION_COOKIE_NAME from its default of 'sessionid'.

Since Django 1.2, you can edit the setting CSRF_COOKIE_NAME from its default of 'csrftoken'

Things to note:

Back to top

CSRF Settings

Vulnabilities:

CSRF attack, Information exposure

One liner:

If an attacker could acquire the CSRF cookie value, due to their long expiry (1 year by default), they could use it to submit a form as a user.

Further detail:

OWASP

Implementation:

Since Django 1.6 you can change the setting CSRF_COOKIE_HTTPONLY to True. This prevents the CSRF cookie from being read by client-side javascript.

Since Django 1.11, you can change the setting CSRF_USE_SESSIONS to True.

Alternatively or for older versions, you can shorten the expiry of the cookie, as of Django 1.7 this is done with the setting CSRF_COOKIE_AGE. You can also try writing custom middleware which regenerates the CSRF-token on a per-request basis.

Things to note:

  • Setting the CSRF token to a shorter expiry may annoy users, as any form they leave in the background for a while or load from a bookmark will fail.
  • If an attacker can access your cookies via javascript you are probably in a lot more trouble than a CSRF attack (eg XSS). The 'CS' part of CSRF makes it clear that this vulnerability is mainly about stopping 'Cross-Site' attacks, not ones where the attacker already has access to your domain via javascript.
  • As Django's own documentation states:

Storing the CSRF token in a cookie (Django’s default) is safe, but storing it in the session is common practice in other web frameworks and therefore sometimes demanded by security auditors.

Back to top

Disuse 'expires' attribute

Vulnerabilities:

Account/session takeover

One-liner:

The expires attribute writes the session cookie to the browser persistently, this can then be used by an attacker or someone sharing the same device.

Further Detail:

OWASP

Implementation:

Change the setting SESSION_EXPIRE_AT_BROWSER_CLOSE to True. This setting has existed since Django 1.4

Back to top

User Management

Username enumeration

Vulnerabilities:

phishing, brute force

One-liner:

You ought to treat non-existent usernames/email addresses the same as existing ones, so as not to reveal this information to an attacker who can for example, look up where else this username is used, if it exists in any data breaches, and can target that user directly.

Further Detail:

OWASP

Implementation:

This vulnerability can occur in several places, including:

  • Registration. You ought not to state that a username/email address already exists as an error message to the user.
  • Forgotten password. You ought not to treat existing usernames/email addresses any differently to non-existing ones.
  • Login. You should not display a specific error message if the username does not exist, but rather a generic message like: "Incorrect username or password"
  • URL Parameter. Applications sometimes have /<username>/ as part of the URL structure, and if you return a 404 error if the username does not exist but a 403 error if the username does exist but you are not allowed to see it, that can be used to enumerate usernames.

Things to note:

  • By default Django already prevents this on the provided forgotten password view, by displaying a success message whether or not the user account exists.

Back to top

Forgot password limit

Vulnerabilities:

DoS, money waste

One-liner:

An attacker can repeatedly hit your 'Forgot password' endpoint, prompting the sending of many emails which could cost you money or lead to a denial-of-service.

Further Detail:

Cloudflare on Denial-of-Service

Implementation:

There are several non-mutually-exclusive methods you can employ:

  • Log each forgotten password request, and only further process the request (and send emails) if there hasn't been a request for that particular username and/or IP address recently
  • Add an (increasing) delay in responding to repeated forgotten password requests
  • Add a CAPTCHA or some other dynamic field that is required before processing the request.

You can see an example implementation here using the PasswordResetView class-based view, introduced in Django 1.11

Things to note:

  • You don't want to leak information about valid and invalid usernames (see username enumeration) so make sure you treat requests for valid and invalid usernames the same.

Back to top

Incorrect password limit

Vulnerabilities:

Brute force

One-liner:

Without a limit, an attacker can repeatedly try different passwords to gain access to a user's account.

Further Detail:

OWASP

Implementation:

There are several non-mutually-exclusive methods you can employ:

  • 'Lock' accounts with too many failed attempts
  • Add an (increasing) delay in responding to repeated login attempts
  • Add a CAPTCHA or some other dynamic field that is required before processing the request
  • Monitor IP address / user agent to detect patterns anomalous to the user's usual usage

You can see an example implementation here

Things to note:

  • You don't want to leak information about valid and invalid usernames (see username enumeration) so make sure you treat requests for valid and invalid usernames the same.

Back to top

Require strong passwords

Vulnerabilities:

Brute force, credential stuffing

One-liner:

Some security auditors require stronger password validation than Django's default. It can also ensure they are less easily guessed by an attacker.

Further Detail:

Wikipedia

Implementation:

Depending on your version of Django, it usually comes with a few useful validators, such as minimum length, and similarity to other user attributes. These can be modified (for example increasing the minimum length):

# settings.py
AUTH_PASSWORD_VALIDATORS = [
    # ...
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
        'OPTIONS': {
            'min_length': 10,
        }
    },
    # ...
]

Or you can write your own. Example validators (tested with v2.0) for requiring special characters and a combination of numbers, uppercase, and lowercase letters can be found here. These can then be added to the AUTH_PASSWORD_VALIDATORS setting.

Things to note:

  • Requiring special characters or other demanding rules in itself can be a vulnerability, as users may write down their password, or re-use a stock 'strong' password across several sites.

Back to top

Transferable Sessions

Vulnerabilities:

XSS, session highjacking

One-liner:

If an attacker acquires the value of the session cookie, they are able to use it to authenticate requests from their own device.

Further Detail:

OWASP

Implementation:

One needs to ensure that each user session is linked to a particular device, so something like a middleware which stores the user agent / IP address of a user at the start of a session, and invalidates the session if it detects a different user agent / IP address on any subsequent request.

See also:

Concurrent Logons

Back to top

Concurrent Logons

Vulnerabilities:

XSS, session highjacking

One-liner:

If an attacker acquires the value of the session cookie, they are able to use it to authenticate requests from their own device at the same time as the original user.

Further Detail:

OWASP

Implementation:

One needs to ensure that when logging in, all existing sessions associated with that user account are deleted. An example implementation can be found here.

Things to note:

  • You may want to implement something like this for reasons other than security - for example to prevent data from being changed simultaneously from two locations, or to prevent the sharing of login credentials.

See also:

Transferable Sessions

Back to top

Require Password Change

Vulnerabilities:

auditability, non-repudiation

One-liner:

When you create an account for a user and provide them with the credentials, the user should be required to change their password when they first login, so that their password is known only to them.

Further Detail:

Techopedia

Implementation:

Add a boolean field to your user model that is set to True when you have manually created their account. When this user logs in, redirect them to the change password form, optionally with a message explaining why. On a successful completion of the change password form, this field can be set to False.

Back to top

Webserver Settings

Rate limit

Vulnerabilities:

ddos, brute force

One-liner:

Too many HTTP requests from a single source is almost certainly not a legitimate human user and can cause your webserver to fail as it tries to process them.

Further Detail:

Wikipedia

Implementation:

You can either apply a policy globally, or at particularly vulnerable endpoints, which include API endpoints, login pages, or other pages requiring user input. This Nginx blog post and the official Apache docs explain how to set it up.

Things to note:

  • Both the Nginx and Apache setups allow for 'bursts' which is a useful feature. Sometimes HTTP requests will bunch up and be received in a short burst, and this allows you to handle these gracefully without returning an error.

Back to top

Disable support for old TLS versions

Vulnerabilities:

Padding oracle attack, BEAST, POODLE

One-liner:

Your webserver might by-default support TLS v1.0 and v1.1, and though almost every modern browser will use v1.2, a security auditor might moan about supporting these older protocols.

Further Detail:

Payment Card Industry Data Security Standard 3.2

Implementation:

Somewhere on your nginx server will be the line:

ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

And you need to remove the TLSv1 and TLSv1.1 statements. If you used Let's Encrypt for your SSL certificate, you may find this configuration in /etc/letsencrypt/options-ssl-nginx.conf

Things to note:

  • If you do use Let's Encrypt and Certbot, the file options-ssl-nginx.conf, won't update as you update the certbot package. The update will instead print out what changes were meant to be made, which you can copy over. Source

Back to top

Disable support for old TLS ciphers

Vulnerabilities:

meet in the middle, downgrade attack

One-liner:

Your TLS setup might by-default support some insecure ciphers which may be allow an attacker to decrypt traffic.

Further Detail:

SSL Labs

Implementation:

Somewhere on your nginx server will be the line:

ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:...

And you need to remove all insecure ciphers. SSL Labs provide a free analysis of your site to show which ciphers you currently support and how secure they are. If you used Let's Encrypt for your SSL certificate, you may find this configuration in /etc/letsencrypt/options-ssl-nginx.conf

Things to note:

  • If you do use Let's Encrypt and Certbot, same as above

Back to top

Admin

Don't use /admin/

Vulnerabilities:

Information exposure

One-liner:

Because it is conventional to use /admin/ as the url for Django's admin site, its presence can alert an attacker to the fact that a site is running Django, allowing them to customise their attack methods.

Further detail:

CWE

Implementation:

In your main urls.py, change the URL at which you include admin.site.urls:

urlpatterns += [
    url(r'^new-secret-location/', admin.site.urls, name='admin')
]

Back to top

Insecure version of jQuery

Vulnerabilities:

XSS

One-liner:

Django < 2.1 admin ships with jQuery version v2.2.3 (/your/static/url/admin/js/vendor/jquery/jquery.min.js) which has known security issues.

Django 3.0 uses jQuery version 3.4.1 which is the latest secure version to-date, but it is worth double-checking if any patched jQuery updates are released, and upgrade if necessary.

Further detail:

CVE

Implementation:

  1. You can update the jQuery file within your STATICFILES folder but this needs to be done every time you update your static files with collectstatic
  2. You can rewrite requests to the insecure version towards an up-to-date version within your webserver configuration, ie mod_rewrite (Apache) / ngx_http_rewrite_module (nginx) For nginx it might look like this:
location /your/static/url/admin/js/vendor/jquery/jquery.min.js {
        return 301 /your/static/url/js/patched-jquery.min.js;
    }

Things to note:

  • Even if using Django >= 2.1 you should still patch this jQuery to handle any new security issues that are found, without having to wait for the Django package to update.

Back to top

Contributing

I am keen to hear suggestions and improvements, please open an issue to discuss!

I am particularly keen on hearing about new vulnerabilities and original ways of mitigating them.