It's undeniable that for the past two decades, businesses of all stripes have been undergoing digital transformations, beefing up their online presences and offering new ways to engage with their customers both on- and off-line. This is a trend that will only be hastened amidst shifting consumer patterns post-COVID 19.
When it comes to growing and scaling online presences, businesses will often use both a front-end and a back-end framework, and rightfully dedicate substantial resources to create smooth, aesthetically pleasing experiences for their users. Unfortunately, website security is often an afterthought, with devastating financial, regulatory, and reputational costs: a 2014 McKinsey report on enterprise IT security estimated a three trillion-dollar ($3,000,000,000,000) cost to the global economy by 2020.
The financial repercussions of lax cyber security are readily apparent when we look at some of the many high profile cyberattacks in recent memory, including the Equifax data breach affecting the sensitive information of 56% of Americans and costing $1.4 billion dollars in legal costs and investigations, and the NotPetya ransomware, which cost companies around the world approximately $1.2 billion dollars. These breaches were caused by stolen credentials, unpatched software, exploitation of novel zero-day vulnerabilities, and misconfigured servers, but the truth is, these are actually not the most common security issues for online web applications. For that, one need only turn to the OWASP Top 10, an annually updated list of the ten most critical and common web security vulnerabilities.
Django security vulnerabilities
In this article, we will explore some of the most prevalent Django security vulnerabilities and how to mitigate them. We've chosen to use Django for this article because Django has been among the top 10 web development frameworks for a number of years, and is based on Python, which is itself a contender for being the most popular and easiest coding language to pick up. Furthermore, Django is popular for its Object-Relational Mapper, or ORM, which enables SQL queries with Pythonic syntax, and its “Django Admin,” an automated admin interface for your site.
SQL Injection Attacks, and safely executing SQL
SQL injection attacks routinely top the OWASP Top 10, and for good reason – most web apps consist of a user interface that connects to one or more databases underlying database driver in the backend. When you consider the cost associated with data breaches, it makes sense that the data stored within these databases is of immense value. Although NoSQL databases are gaining in popularity, the vast majority of enterprise databases in use today are still SQL relational databases, which means most web applications will be able to run SQL code, too.
Best practices for SQL in Django is to use the built-in ORM QuerySet API, although there are certainly scenarios where you'd like to run raw or custom SQL. In such cases, using
Manager.raw(raw_query, params=None, translations=None) will create a RawQuerySet instance that can be used just like a QuerySet.
Two common mistakes that can lead to injection attacks are using string formatting and quote placeholders with a raw query, as seen below:
query = 'SELECT * FROM table WHERE column = %s' % userinput Class.objects.raw(query)
Relying on string formatting opens your application to the possibility of attackers using a variety of malicious inputs execute arbitrary sql code. The classic example would be
‘SELECT * FROM users WHERE username= %s AND password= %s' % username, password. If the attacker submits
‘YouGotPwned' or ‘1'=='1';-- as the password, or even simpler,
‘admin';--as the username, they can effectively bypass your login authentication altogether, gaining access to your database much too easily.
Adding quotes around
%s like below doesn't make the app any safer, because the attacker need only omit the opening quotation mark before inserting a semicolon and proceeding to any malicious code they wish, before throwing a double dash to comment out any further SQL validation you may have set up.
query = "SELECT * FROM table WHERE column = '%s'"
Instead, use parameterized inputs in your SQL query like the following user input:
userinput = form_data.get("user_input")
Class.objects.raw('SELECT * FROM table WHERE column = %s', [userinput])
Parameterization in Django (via QuerySet and other built-in ORM functions) and other frameworks protects against SQL injection by converting any potentially unsafe user inputs into a safe form, like a string, that will not be treated and executed as code.
Django comes with a fairly robust standard authentication suite, but as authentication issues remain one of the more common security threats, there are a few pitfalls to avoid. One is the use of the
redirect_authenticated_user flag, which ensures that a logged-in user will be redirected from the login page if accessed directly.
Setting this flag needs to be used in conjunction with
permission_required(permission_name, raise_exception=True) to avoid a redirect loop, and ideally your images and favicons should be hosted on a separate domain or subdomain too, to prevent third-party sites from ascertaining whether your users are authenticated by requesting redirect URLs to your images.
It is also a good idea to rate-limit authentication attempts with django-ratelimit-backend to prevent automated brute force testing of credentials gained from other data breaches - possibly the most basic Django admin exploit.
Preventing Sensitive Data Exposure
Passwords should always be salted and hashed with an algorithm like Bcrypt, Argon2, or PBKDF2. These algorithms make it difficult for GPUs, ASICs, and FPGAs to brute force your customers’ login credentials. Fortunately, Django uses PDKDF2 by default, but you can always go to
settings.py and specify the algorithm you wish to use under PASSWORD_HASHERS. While modifying
settings.py, it is a good idea to set
SECURE_SSL_REDIRECT = TRUE,
SESSION_COOKIE = TRUE, and
CSRF_COOKIE_SECURE = TRUE to ensure HTTPS is used.
XML External Entities
Django version 2.2 onwards doesn’t allow DTDs (document type definitions), entity expansion, or fetching of external entities, but it is important to note that widely used XML parsing libraries like
xml.etree.ElementTree have known security vulnerabilities with devastating repercussions. It is recommended to use
defusedexpat instead, wherever possible.
defusedxml is a Python package specifically written to address common vulnerabilities in Python’s default XML parser libraries, and should be used on any server that parses untrusted XML.
defusedxml is similar, but protects specifically against entity expansion Denial of Service (DoS) attacks. These two packages greatly reduce the likelihood of suffering billion laughs, quadratic blowup, external entity expansion, DTD retrieval, and decompression bomb attacks.
Large organizations will often use Git or another form of version control to assist their development efforts. On the server side, using absolute file paths in Django's
settings.py will often result in Django apps working in some, but not all, developer environments. The solution here is to use
os.path.join() to create relative paths. Although it may be tempting to create path strings via string concatenation, doing so is a security risk as well.
Consider the following code:
path = "User/Documents"
os.path.join(path, "/home", "file.txt")
This may look the same as the output of
path += ‘/home’ + file, but is safer because as a user-uploaded file, file can have any name chosen by the user, even including malicious code.
os.path.join() will explicitly treat its values as a directory path and never as executable code.
Be sure to set
DEBUG = FALSE in any production environments (it is TRUE by default). This not only helps your web server run faster, but also reduces the risk of malicious attackers seeing error messages that can inform future attacks. You’d want to set up your
SERVER_EMAIL settings within settings.py to ensure you can still access error logs (make sure to replace any default passwords with a complicated version). Furthermore, unless absolutely necessary, it is best not to hard code what Django version you're using. This will always help avoid your web application from being exploitable to older, known vulnerabilities or malicious scripts.
A good way to keep your production and dev environment settings secure is to create two separate configuration files and use an environment variable,
DJANGO_SETTINGS_MODULE, to toggle as needed. When in doubt, always consult the official Django documentation.
Cross Site Scripting (XSS)
Fortunately for us, Django’s built-in
render() function allows the passing of variables into templates before being displayed as HTML.
For example, just adding the following line will ensure your forms are secure against DOM-based XSS attacks:
from django.shortcuts import render, render_to_response
Cross Site Request Forgery (CSRF)
Another common web security vulnerability is Cross Site Request Forgeries (CSRF). CSRF attacks commonly used to spoof actions of legitimate users, and involve sending illegitimate requests with legitimate cookies. Anti-CSRF tokens, making sure
GET requests cannot result in state changes (i.e. updating or modifying any values in your database), and setting the cookie SameSite attribute to Strict are common techniques for mitigating CSRF attacks.
By default, Django has a
MIDDLEWARE setting that helps ensure safe GET request functionality, but if you must disable the setting, be sure to use
csrf_protect() and the
csrf_token tag within your
<form> elements. Another rule of thumb is to set reasonable expiration times for your cookies and access tokens, so even if an attacker manages to obtain a user's cookie, there will be a limited timeframe for malicious CSRF attacks to be executed.
The bottom line
In conclusion, Django offers many good security-minded functions right out of the box, without sacrificing ease of development and integration with both front-end and back-end components. Although by no means comprehensive, following the suggestions in this article and following a security-minded development process can help protect your company from Django CVE (Common Vulnerabilities and Exposures), multi-million dollar data breaches and years of negative publicity.