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.
- 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
SSL-stripping, man-in-the-middle
Forces browsers to redirect non-HTTP traffic to HTTPS
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;
- 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 yourlocation /static/
orlocation /uploads/
blocks
XSS
You whitelist valid sources of executable scripts.
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.
- 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
XSS
Preventing the execution of malicious files.
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";
CSRF attack, information exposure
Adding this attribute prevents sending cookies (like Django's session id) when requesting resources (eg. images, fonts, scripts) hosted elsewhere.
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
- 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
Session hijacking, man-in-the-middle
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.
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
- 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
ifDEBUG == True
in yoursettings.py
works. - Modifying the CSRF cookie is redundant if you put the CSRF cookie in the session cookie
Information exposure
The Django default names for cookies mean than an attacker knows to probe Django-specific weaknesses
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'
- Renaming the CSRF cookie is redundant if you put the CSRF cookie in the session cookie
CSRF attack, Information exposure
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.
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.
- 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.
Account/session takeover
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.
Change the setting SESSION_EXPIRE_AT_BROWSER_CLOSE
to True
. This setting has existed since Django 1.4
phishing, brute force
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.
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.
- By default Django already prevents this on the provided forgotten password view, by displaying a success message whether or not the user account exists.
DoS, money waste
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.
Cloudflare on Denial-of-Service
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
- 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.
Brute force
Without a limit, an attacker can repeatedly try different passwords to gain access to a user's account.
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
- 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.
Brute force, credential stuffing
Some security auditors require stronger password validation than Django's default. It can also ensure they are less easily guessed by an attacker.
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.
- 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.
XSS, session highjacking
If an attacker acquires the value of the session cookie, they are able to use it to authenticate requests from their own device.
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.
XSS, session highjacking
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.
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.
- 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.
auditability, non-repudiation
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.
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
.
ddos, brute force
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.
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.
- 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.
Padding oracle attack, BEAST, POODLE
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.
Payment Card Industry Data Security Standard 3.2
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
- 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
meet in the middle, downgrade attack
Your TLS setup might by-default support some insecure ciphers which may be allow an attacker to decrypt traffic.
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
- If you do use Let's Encrypt and Certbot, same as above
Information exposure
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.
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')
]
XSS
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.
- You can update the jQuery file within your
STATICFILES
folder but this needs to be done every time you update your static files withcollectstatic
- 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;
}
- 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.
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.