18 Apr, 2019

Django vs. the OWASP Top 10 - Part 1

by Michael Andino

Part 1 of this series will focus on Django’s built-in mitigations for some of the most common risks listed in the OWASP Top 10, while part 2 will focus on misconfigurations and insecure coding practices. For those unaware, the OWASP Top 10 is a list of the most common web application security weaknesses found in real-world applications and APIs. The risks are listed in order from A1 - A10, with A1 being the most prevalent risk. The 2017 version of the OWASP Top 10 and Django 2.2 pseudocode is used for the examples contained in this blog post.

Django’s built-in protections

A1 - Injection

Injection has been one of the highest risks listed in the Top 10 for over ten years and is listed as the A1 risk in the latest version. Injection occurs when untrusted data is used to execute unintended commands. A common example of this is SQL Injection. Django provides a built-in Object-Relational Mapping layer that protects against injection attacks via parameterization. To use the ORM, you have to set up a Model.

Models are used to define fields and behaviors of tables in your database.

 class Person(models.Model):
        first_name = models.CharField(max_length=30)
        last_name = models.CharField(max_length=30)

Once a model is defined, methods are provided for CRUD interactions.

newPerson = Person.objects.create(first_name=request.user.first_name, last_name=request.user.last_name)
newPerson.save()

The request.user.first_name and request.user.last_name parameters used in the create() method are escaped to prevent SQL injection attacks.

In addition to the ORM methods, Django allows you to enter raw SQL with the raw() and execute() methods. These methods, if done correctly, can use parameterization as well.

from django.db import connection
...
def custom_sql(self):
  with connection.cursor() as cursor:
    cursor.execute("INSERT INTO Person (first_name, last_name) \
                    VALUES (%s, %s)", [param1, param2])  
...

When using the execute() method, it is important to remember that the format specifiers (%s in this example) should be used without single quotes '%s'. It is also worth mentioning that while this may look like string interpolation, it is not due to the escaping performed on the parameters.

A2 - Broken Authentication

Broken Authentication and Session Management is another risk that is no stranger to the OWASP Top 10. Django has a basic authentication system that provides objects for authentication, authorization, and session management, but it lacks in other areas. The framework does not provide password strength checking, throttling of login attempts, or two-factor authentication. These features have been implemented in third-party packages, but they are not provided out of the box.

To enable Django’s authentication framework you need to add a few things to the settings.py file under the INSTALLED_APPS and MIDDLEWARE section:

INSTALLED_APPS = [
  ...
  'django.contrib.auth',
  'django.contrib.contettypes',
  ...
  ]
  
MIDDLEWARE = [
  ...
  'django.contrib.sessions.middleware.SessionMiddleware',
  'django.contrib.auth.middleware.AuthenticationMiddleware',
  ...
  ]

Because the framework lacks certain protections and features, this risk will be covered further in Part 2.

A3 - Sensitive Data Exposure

If an application handles or stores sensitive data, it is a target. Django provides settings to keep sensitive data secure while in transit and at rest, but they are not all enabled by default.

If the application is storing passwords, the PASSWORD_HASHERS list can be added to the setting.py file in order to specify the hashing algorithm. By default, Django uses the PBKDF2 algorithm, but other algorithms can be added to the list to check existing/old passwords.

There are a few other settings to look for in the settings.py file:

  • SECURE_SSL_REDIRECT = True This will redirect all HTTP traffic to HTTPS
  • SESSION_COOKIE_SECURE = True This will ensure that the session cookie is only sent over HTTPS
  • CSRF_COOKIE_SECURE = True This will ensure that the CSRF Token is only sent over HTTPS

Another method that can be used for data in transit is the @sensitive_variables() decorator. The decorator can be added before a function to prevent the values of the sensitive variables from showing up in error reports. The variable names of sensitive values are passed into the decorator as arguments.

@sensitive_variables('first_name', 'last_name', 'credit_card', 'ssn', 'blood_type', 'etc')
def save_user_info(user)
    first_name = user.first_name
    last_name = user.last_name
    credit_card = user.ccn
    ssn = user.ssn
    ...

If all variables in the method are sensitive, the sensitive_variables decorator can be added before a function without arguments.

A4 - XML External Entities

XML External Entity (XXE) attacks can lead to remote command execution, denial of service, and data exfiltration. Django version 2.2 is not vulnerable to XXE attacks on its own because the XML deserializer does not allow DTDs, fetching of external entities, or the ability to perform entity expansion. However, if a Django application accepts XML, it is worth looking into as it may be using a third-party library for XML parsing. Many third-party libraries for XML parsing are not protected against XXE attacks by default.

A5 - Broken Access Control

Broken Access Control is a combination of Missing Function Level Access Control and Insecure Direct Object References (IDOR). The Django Framework has a simple permissions system that allows users with specific permissions/roles/groups to access protected files or methods.

If django.contrib.auth is in the list of INSTALLED_APPS located in the settings.py file, then Django will automatically create, add, change, delete, and view permissions for each Model in the application. For example, let’s look at an application with an app_label = appa, and appa has a model named BlogPost. When users are created these permissions can be assigned to them.

...
def add_new_blogger(request):
    user = Users.objects.create_user(request.first_name, request.email, request.password)
    user.user_permissions.add('appa.create_blogpost')
    user.save()
...

In the example, a user was created with the view_blogpost permission. To prevent a user from deleting a blog post, the @permission_required() decorator can be added before the delete_blog() function to check the user’s permissions.

@permission_required('appa.delete_blogpost')
def delete_blog(request, blog_id):
    ...

Other decorators such as @login_required can be used to ensure that unauthenticated users cannot view or use the portions of the application that require authentication. Third-party packages such as django-guardian are available to extend the functionality of Django’s permission systems.

A6 - Security Misconfigurations

Security Misconfigurations in Django applications can lead to Sensitive Data Exposure, Broken Access Control, Cross-Site Scripting, and more. Because this will be the majority of Part 2 of this blog entry, I will list one thing to check that has plagued admins in the past. Make sure debug is turned off:

DEBUG = False 

A7 - Cross-Site Scripting

Cross-Site Scripting (XSS) is becoming less common as more frameworks are providing built-in protections against it. Django provides templates that protect against XSS by escaping special characters before being displayed in HTML.

Python variables can be passed from a function to template by using the Django render() function.

from django.shortcuts import render

def say_hi(request):
    first_name = request.user.first_name
    last_name = request.user.last_name
    context = {
        'first_name': first_name,
        'last_name': last_name,
    }
    return render(request, 'Profile/welcome.html', context)

The example code above is sending the variables in context to the welcome.html. When using double open and close curly braces within templates, {{}}, Django escapes the values of the variables.

...
<h1> Hi , !</h1>
...

A8 - Insecure Deserialization

Insecure Deserialization attacks occur when untrusted data is deserialized into an object where the logic can be manipulated or remote code can be executed. Django contains a basic serialization framework that can be used to serialize models into other formats. When deserializing, the framework will check if the fields in the serialized data exist on a model. If the fields do not match, an error will be raised. This is the only protection that Django provides against Insecure Deserialization attacks. The best way to prevent these attacks is not to accept serialized data from untrusted sources.

Because Django developers often use other libraries for serialization, such as Python’s pickle or third-party libraries, we will discuss this topic further in Part 2.

A9 - Using Components With Known Vulnerabilities

Using components with known vulnerabilities is one of the most common risks in the Top 10. At the time this post was written, Django version 2.2 did not contain known vulnerabilities, but most applications use third-party libraries, so they don’t have to re-invent the wheel. Scanners such as dependency-check should be used to find third-party packages with know vulnerabilities and update those libraries. We will dig deeper into this topic in Part 2.

A10 - Insufficient Logging and Monitoring

When your application has insufficient logging and monitoring, attacks and suspicious activity can go unnoticed. By default, Django uses the Python native logging module for system logging. When DEBUG is disabled in settings.py, Django emails all ERROR and CRITICAL log messages to the site admins. In Part 2 we will further configure Django’s logging and enable django.security.* logging messages.