SammyFold

SammyFold AI

👋 Hi! Looking to build something great? Ask me about our services, pricing, or how we can help your business.

Dev Concepts

Django Security Explained Simply: 11 Ways to Protect Your App

CSRF, SQL injection, XSS, HTTPS, secrets and more — Django security explained in plain English with everyday examples, code you can copy and interview questions.

Building a website is like building a house. Making it look nice is only half the job — you also need locks on the doors, bars on the windows and a guard at the gate. Website security is all the "locks" that stop strangers from stealing data, pretending to be someone else, or breaking your app.

The good news: Django comes with many of these locks already installed. This guide walks through the 11 areas every Django developer should know, explained so that even if you're brand new (or not technical at all) you'll understand why each one matters.

Who is this for? Beginners, students preparing for interviews, and business owners who want to know what "a secure app" actually means. Developers can copy every code sample.

A few words first

Word What it means in plain English
Attacker / hacker Someone trying to misuse your website.
Request Every time a browser asks your site for a page or sends a form, that's a request.
settings.py Django's control panel — one file where you switch features on and off.
Middleware Checkpoints every request passes through, like security guards in a corridor.
Production The real, live website that customers use (as opposed to your laptop).
Cookie A small note the website stores in your browser, e.g. "this person is logged in".

1. Django's built-in protections

Django blocks three of the most common attacks automatically. You just have to not switch them off.

CSRF — stopping forged requests

In plain English: imagine you're logged in to your bank in one tab. You open a dodgy website in another tab, and it secretly submits a "send money" form to your bank using your logged-in session. That trick is called Cross-Site Request Forgery (CSRF).

How Django protects you: every form gets a secret one-time token. The bank (your site) only accepts a form if it carries the correct token — and the dodgy site can't know it.

# settings.py — this middleware is ON by default. Leave it there.
MIDDLEWARE = [
    # ...
    'django.middleware.csrf.CsrfViewMiddleware',
]
<!-- In every form that uses method="post" -->
<form method="post">
  {% csrf_token %}
  ...
</form>

Beginner mistake: adding @csrf_exempt to "fix" a 403 error. That removes the lock. Find out why the token is missing instead.

SQL injection — stopping database tricks

In plain English: a login box asks for your username. An attacker types something like ' OR 1=1 -- instead. If the code glues that text straight into a database command, the attacker can trick the database into "log me in as anyone" or "delete everything".

How Django protects you: the ORM (Django's way of talking to the database) never glues text in. It always sends your values separately from the command, so they are treated as data — never as instructions.

# Safe: the ORM handles the value for you
User.objects.filter(username=username_from_form)

XSS — stopping injected scripts

In plain English: someone posts a "comment" that is actually JavaScript code. If the site shows that comment as-is, the code runs in every visitor's browser and can steal their login. That's Cross-Site Scripting (XSS).

How Django protects you: templates auto-escape everything. <script> becomes harmless text that is displayed, not run.

<p>{{ comment.text }}</p>          <!-- Safe: escaped automatically -->
<p>{{ comment.text|safe }}</p>     <!-- DANGER: never use |safe on things users typed -->

2. Passwords & logins (authentication)

In plain English: authentication = proving who you are (logging in). Authorization = what you're allowed to do once you're in (e.g. only managers can see salaries).

Good news first: Django never stores real passwords. It stores a scrambled "fingerprint" (a hash), so even if someone steals the database they don't get the passwords.

Make users choose strong passwords with validators:

# settings.py
AUTH_PASSWORD_VALIDATORS = [
    # Not too similar to the username or email
    {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
    # At least 12 characters
    {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
     'OPTIONS': {'min_length': 12}},
    # Not a well-known password like "password123"
    {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
    # Not only numbers
    {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
]

Then lock pages to the right people:

from django.contrib.auth.decorators import login_required, permission_required

@login_required                                   # must be logged in
@permission_required('payroll.view_salary')       # and have this permission
def salary_report(request):
    ...

Beginner mistake: hiding a button in the template but not protecting the view. Anyone can still type the URL. Always protect the view.

3. HTTPS — the padlock in the address bar

In plain English: without HTTPS, data travels across the internet like a postcard — anyone along the way can read it. HTTPS puts it in a sealed envelope (encryption).

# settings.py (production)
SECURE_SSL_REDIRECT = True          # send http:// visitors to https://
SESSION_COOKIE_SECURE = True        # login cookie only travels over HTTPS
CSRF_COOKIE_SECURE = True           # CSRF cookie only travels over HTTPS
SECURE_HSTS_SECONDS = 31536000      # tell browsers "always use HTTPS here" for 1 year
SECURE_HSTS_INCLUDE_SUBDOMAINS = True

Two real-world tips that save hours:

  • Hosted on Railway, Heroku or behind a load balancer? Add the line below, otherwise Django thinks every request is plain HTTP and redirects forever ("too many redirects"):
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
  • Start HSTS small (e.g. 3600 seconds) and increase it once you're sure every subdomain works on HTTPS. Browsers remember HSTS, so a mistake can lock visitors out of a subdomain for the whole period.

4. Never trust user input (validation)

In plain English: treat everything a user types or uploads like a parcel from a stranger — check it before you open it.

from django import forms
from django.contrib.auth.models import User

class SignUpForm(forms.ModelForm):
    class Meta:
        model = User
        fields = ['username', 'email']

    def clean_email(self):
        email = self.cleaned_data['email'].lower()
        # iexact = ignore capital letters, so "Ama@x.com" and "ama@x.com" count as the same
        if User.objects.filter(email__iexact=email).exists():
            raise forms.ValidationError('An account with this email already exists.')
        return email

File uploads are the riskiest input. Limit the type and size:

from django.core.validators import FileExtensionValidator

def validate_size(file):
    if file.size > 5 * 1024 * 1024:   # 5 MB
        raise forms.ValidationError('File too large (max 5 MB).')

class CVForm(forms.Form):
    cv = forms.FileField(validators=[FileExtensionValidator(['pdf']), validate_size])

Beginner mistake: checking only in JavaScript. Attackers skip your website and send requests directly — always validate on the server.

5. Database security

The ORM is safe (see SQL injection above). Trouble starts when you write raw SQL by hand:

# SAFE: the value is passed separately as a parameter
users = User.objects.raw('SELECT * FROM auth_user WHERE id = %s', [user_id])

# DANGEROUS: never build SQL with f-strings or +
users = User.objects.raw(f'SELECT * FROM auth_user WHERE id = {user_id}')

Also:

  • Give your app's database user only the permissions it needs (it doesn't need to create other databases).
  • Keep backups, and test that you can actually restore them.

6. Error pages that don't leak secrets

In plain English: when something breaks during development, Django shows a detailed yellow error page — your code, your settings, sometimes passwords. That's helpful on your laptop and a gift to attackers on a live site.

# settings.py (production)
DEBUG = False
ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com']   # only answer to your real domains

Create friendly error pages so visitors see something nice instead: put 404.html and 500.html in your templates folder. Django uses them automatically when DEBUG = False.

7. Protecting the admin panel

The Django admin is the master key to your data, so guard it well:

# urls.py — move it away from the obvious /admin/ address bots try first
urlpatterns = [
    path('office-portal-7f3a/', admin.site.urls),
]

Moving the URL keeps away automated bots, but it is not a lock by itself. Real protection comes from:

  • Strong passwords, and giving staff access only to people who need it.
  • Two-factor authentication (a code from your phone) — Django doesn't include it, but packages like django-otp or django-two-factor-auth add it.
  • Limiting failed logins with a package like django-axes, so nobody can guess passwords forever.
  • Allowing only your office/VPN IP addresses, if your hosting supports it.

8. Logging — your security camera

In plain English: logs are a diary of what happened. When something goes wrong, they tell you who, what and when.

Django already reports suspicious activity (like requests for unknown domains) through the django.security logger. Send it somewhere you'll see it:

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'console': {'class': 'logging.StreamHandler'},
    },
    'loggers': {
        'django.security': {'handlers': ['console'], 'level': 'INFO'},
    },
}

Hosting tip: on platforms like Railway or Heroku the server's files are wiped on every deploy, so logging to a security.log file loses your history. Log to the console and read it in your hosting dashboard (or send it to a logging service).

Track failed logins too:

import logging
from django.contrib.auth.signals import user_login_failed
from django.dispatch import receiver

logger = logging.getLogger('django.security')

@receiver(user_login_failed)
def log_failed_login(sender, credentials, request, **kwargs):
    ip = request.META.get('REMOTE_ADDR') if request else 'unknown'
    logger.warning('Failed login attempt from %s', ip)

9. Keeping packages up to date

In plain English: your app is built from many packages (Django itself, payment libraries…). When a security hole is found in one, the fix arrives as an update. Old versions = known holes.

pip list --outdated          # what has a newer version?
pip install --upgrade django # update one package (then run your tests!)

pip install pip-audit        # a free scanner (install it once)
pip-audit                    # lists packages with known vulnerabilities

Pin exact versions in requirements.txt so every server runs the same, tested code, and turn on Dependabot in GitHub to get automatic update alerts.

10. Securing APIs (Django REST Framework)

APIs power mobile apps and other systems. Three rules: know who's calling (authentication), decide what they can do (permissions), and stop spam (throttling).

# settings.py
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.TokenAuthentication',
    ],
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',   # locked unless a view says otherwise
    ],
    'DEFAULT_THROTTLE_CLASSES': [
        'rest_framework.throttling.AnonRateThrottle',
        'rest_framework.throttling.UserRateThrottle',
    ],
    'DEFAULT_THROTTLE_RATES': {
        'anon': '100/hour',     # visitors who aren't logged in
        'user': '1000/hour',    # logged-in users
    },
}

Note: token authentication also needs 'rest_framework.authtoken' in INSTALLED_APPS and a migrate. If a browser app on another domain calls your API, allow only that domain (for example with django-cors-headers) — never "allow everyone".

11. Secrets & environment variables

In plain English: your SECRET_KEY, database password and API keys are the keys to the building. Never write them inside your code, because code gets shared — on GitHub, with teammates, with freelancers.

Instead, store them as environment variables (settings that live on the server, outside the code):

import os
from django.core.exceptions import ImproperlyConfigured

def get_env_variable(name):
    try:
        return os.environ[name]
    except KeyError:
        raise ImproperlyConfigured(f'Set the {name} environment variable')

SECRET_KEY = get_env_variable('SECRET_KEY')
DATABASE_URL = get_env_variable('DATABASE_URL')
  • Keep a local .env file for development, and add .env to .gitignore so it never reaches GitHub.
  • If a secret ever leaks, change (rotate) it immediately — deleting the commit isn't enough, because copies may already exist.

Bonus: Django's free security check

Before every launch, run:

python manage.py check --deploy

Django inspects your settings and lists what's missing (HTTPS settings, DEBUG, cookies and more). It takes seconds.

Quick checklist

Area Done when…
CSRF / XSS / SQL injection Middleware on, {% csrf_token %} in forms, no |safe on user input, no f-string SQL
Passwords Validators set, views protected with login_required / permissions
HTTPS Redirect, secure cookies, HSTS, proxy header on Railway/Heroku
Input Forms validate on the server, uploads limited by type and size
Errors DEBUG = False, ALLOWED_HOSTS set, custom 404/500 pages
Admin Custom URL, strong passwords, 2FA, login-attempt limits
Logging Security events logged somewhere you actually read
Packages Versions pinned, pip-audit clean
APIs Authentication, permissions and throttling configured
Secrets In environment variables, .env in .gitignore

Interview questions on Django security

What is CSRF and how does Django prevent it?

CSRF is when another website tricks a logged-in user's browser into submitting a request to your site. Django's CsrfViewMiddleware requires a secret token (added with {% csrf_token %}) on every unsafe request (POST, PUT, DELETE). A foreign site can't read that token, so its forged request is rejected with a 403.

How does the Django ORM protect against SQL injection?

It uses parameterised queries: the SQL command and the user's values are sent to the database separately, so values are always treated as data, never as SQL. The risk returns only if you build raw SQL with string formatting — use %s placeholders with a params list instead.

Why must DEBUG be False in production?

With DEBUG = True, error pages show source code, settings and request details to anyone. It also keeps extra data in memory. In production it must be False, together with a correct ALLOWED_HOSTS list.

What's the difference between authentication and authorization?

Authentication answers "who are you?" (logging in). Authorization answers "what are you allowed to do?" (permissions, groups, is_staff). A user can be authenticated but still not authorised to view a page.

What does HSTS do, and what's the risk of setting it wrongly?

HSTS tells browsers to only ever use HTTPS for your domain for a set time. If you enable it (especially with INCLUDE_SUBDOMAINS or preload) before every subdomain supports HTTPS, browsers will refuse to load those subdomains until the time runs out — so start with a small value and increase it.


Read next: Python & Django interview questions explained for beginners.

Based on the Django Security Mastery cheat sheet. Learn more from the official Django security docs and the OWASP Top 10.

Need a security review of your own Django app? Talk to SammyFold.

Agyemang SamuelSammyFold Technology · Building software for African businesses. Got a question about this post? Use the Chat tab or contact us.

Install SammyFold Technology

Launch like a native app — no browser needed
Reopen pages you've visited, even when offline
Faster, full-screen experience on any device
Start a project or leave a review in one tap
Install on your laptop / PC

Works in Chrome and Edge on Windows, Mac and Linux.

1 Look for the install icon at the right end of your browser's address bar
2 Or open the browser menu ⋮ and choose "Install SammyFold Technology…"
3 Click "Install" to confirm — the app opens instantly
Add to iPhone / iPad

On another device? Open sammyfoldtech.com in Safari on your iPhone first.

1 Tap the Share button at the bottom of Safari
2 Scroll and tap "Add to Home Screen"
3 Tap "Add" — SammyFold is installed instantly