Common Security Mistakes in Django and How to Fix Them

Ahmad W KhanAhmad W Khan
5 min read

Django is a powerful and secure web framework, but like any tool, its security depends on how developers use it. Many security vulnerabilities arise from misconfigurations, poor coding practices, or a lack of awareness about potential threats. Let’s go through some of the most common security mistakes in Django applications and how to fix them.

1. Keeping DEBUG=True in Production

The Mistake

When DEBUG=True, Django provides detailed error messages that include sensitive information like environment variables, database connection details, and application settings. This is great for debugging but dangerous in production.

The Fix

Always set DEBUG=False in production and ensure sensitive information isn’t leaked.

DEBUG = False
ALLOWED_HOSTS = ["yourdomain.com"]

Additionally, use environment variables to manage settings securely:

import os
DEBUG = os.getenv("DJANGO_DEBUG", "False") == "True"

2. Exposing Secret Keys in Public Repositories

The Mistake

Many developers accidentally commit settings.py to version control with their SECRET_KEY exposed. Attackers can use this key to generate valid session cookies or even sign malicious requests.

The Fix

Move sensitive values to environment variables and keep them out of version control.

import os
SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "your-default-secret-key")

Use a .env file and load it with django-environ:

# .env file
DJANGO_SECRET_KEY=your-very-secret-key

And load it in settings.py:

import environ
env = environ.Env()
environ.Env.read_env()

SECRET_KEY = env("DJANGO_SECRET_KEY")

Also, add *.env to your .gitignore to prevent committing secrets.


3. Using Default Database Configurations

The Mistake

By default, Django does not enforce SSL/TLS encryption in database connections, leaving data vulnerable to interception.

The Fix

Use strong database configurations, especially for production:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': env('DB_NAME'),
        'USER': env('DB_USER'),
        'PASSWORD': env('DB_PASSWORD'),
        'HOST': env('DB_HOST'),
        'PORT': env('DB_PORT'),
        'OPTIONS': {
            'sslmode': 'require',  # Enforce SSL connection
        },
    }
}

For PostgreSQL, ensure that you require SSL in your database settings.


4. Not Using Secure Password Hashing

The Mistake

Using weak password hashing algorithms or storing plain-text passwords in the database.

The Fix

Django uses PBKDF2 by default, but you can switch to Argon2, which is more resistant to brute-force attacks.

PASSWORD_HASHERS = [
    'django.contrib.auth.hashers.Argon2PasswordHasher',
    'django.contrib.auth.hashers.PBKDF2PasswordHasher',
    'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
    'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
]

Never store passwords in plaintext and always use Django's set_password() to hash passwords before saving.

from django.contrib.auth.models import User
user = User.objects.create(username='secureuser')
user.set_password('Secure@123')  # Hashes the password
user.save()

5. Weak CSRF Protection

The Mistake

Forgetting to use CSRF tokens on forms, allowing attackers to perform Cross-Site Request Forgery (CSRF) attacks.

The Fix

Ensure that every form submission includes a CSRF token:

<form method="post">
    {% csrf_token %}
    <input type="text" name="username">
    <button type="submit">Submit</button>
</form>

For API-based requests, ensure the CSRF middleware is properly handled by using CSRF exempt views only when necessary:

from django.views.decorators.csrf import csrf_exempt

@csrf_exempt
def my_view(request):
    pass  # Only use if absolutely necessary

Use Django’s built-in CSRF protection headers for AJAX requests:

const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value;
fetch('/api/endpoint/', {
    method: 'POST',
    headers: { 'X-CSRFToken': csrftoken },
    body: JSON.stringify({ data: "value" })
});

6. Allowing SQL Injection

The Mistake

Using raw SQL queries with unescaped input allows SQL injection, where attackers can manipulate queries to extract or modify data.

# Vulnerable code
username = request.GET.get('username')
query = f"SELECT * FROM users WHERE username = '{username}'"
cursor.execute(query)  # SQL Injection risk

The Fix

Always use Django's ORM instead of raw queries:

from django.db.models import Q
User.objects.filter(Q(username=username))

If you must use raw SQL, use parameterized queries:

cursor.execute("SELECT * FROM users WHERE username = %s", [username])

7. Not Using HTTPS

The Mistake

Serving your site over HTTP exposes sensitive user data, including login credentials and session cookies.

The Fix

Force HTTPS by setting SECURE_SSL_REDIRECT:

SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

Redirect all HTTP traffic to HTTPS in Nginx:

server {
    listen 80;
    server_name yourdomain.com;
    return 301 https://$host$request_uri;
}

Use Let's Encrypt or other SSL providers to enable HTTPS.


8. Using Default Django Admin URL

The Mistake

Leaving the Django Admin panel exposed at /admin/ makes it an easy target for brute-force attacks.

The Fix

Change the default admin URL:

urlpatterns = [
    path("secureadmin/", admin.site.urls),
]

Additionally, restrict access using IP whitelisting:

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django_ip_restrict.middleware.IPRestrictMiddleware",
]

IP_RESTRICTOR_ALLOW_LIST = ['YOUR_IP_ADDRESS']

Use Django Admin Honeypot to mislead attackers:

pip install django-admin-honeypot

And add to your urls.py:

import honeypot
urlpatterns = [
    path("admin/", include("honeypot.urls", namespace="honeypot")),
]

9. Poor Session Management

The Mistake

Django’s default session storage allows session hijacking if not properly secured.

The Fix

  • Set session cookies to be HTTP-only and secure:
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SECURE = True
  • Use database-backed sessions instead of storing them in cookies:
SESSION_ENGINE = 'django.contrib.sessions.backends.db'
  • Enable session expiration:
SESSION_COOKIE_AGE = 3600  # 1 hour
SESSION_EXPIRE_AT_BROWSER_CLOSE = True

Final Thoughts

Django provides excellent security features out-of-the-box, but it's up to developers to configure and use them properly. By following these best practices, you can significantly reduce security risks and build a safer Django application.

Security isn’t a one-time effort—it’s a continuous process. Regularly audit your code, stay updated with Django security releases, and use tools like django-secure, bandit, and django-security-checker to automate security checks.

For more such tips, visit me at AhmadWKhan.com

0
Subscribe to my newsletter

Read articles from Ahmad W Khan directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ahmad W Khan
Ahmad W Khan