I Almost Wrote a SQL Injection Vulnerability — Here's How Django Saved Me (And When It Won't)

March 14, 2026, 10:57 a.m.

A few weeks into building my Django blog, I needed to add a search feature. My first instinct was to do something like this:

query = request.GET.get('q')
posts = Post.objects.raw(f"SELECT * FROM post_post WHERE title LIKE '%{query}%'")

It worked. I tested it, results came back, I moved on. Then I read about SQL injection.


What actually happens when you do this

If a user types ' OR '1'='1 into your search bar, that query becomes:

SELECT * FROM post_post WHERE title LIKE '%' OR '1'='1%'

Which returns every row in your database. Every post, draft, private entry, all of it. It gets worse. With the right input, an attacker can do this:

'; DROP TABLE post_post; --

Your data is gone. No warning, no confirmation, no recovery unless you have backups.

Why Django ORM protects you by default

Django's ORM never interpolates user input directly into SQL. When you write:

posts = Post.objects.filter(title__icontains=query)

Django generates a parameterized query under the hood:

SELECT * FROM post_post WHERE title LIKE %s  -- query is passed separately

The database receives the query structure and the user input as two separate things. The input is treated as data, never as code. There's nothing to inject. This is the same principle as working with typed function signatures, the shape of the operation is fixed, only the values change.


When Django won't save you

Django's protection only applies when you use the ORM correctly. The moment you write raw SQL with string formatting, you're on your own:

# ❌ Vulnerable
Post.objects.raw(f"SELECT * FROM post_post WHERE title = '{query}'")
# ✅ Safe — parameterized
Post.objects.raw("SELECT * FROM post_post WHERE title = %s", [query])

Same goes for cursor.execute(). Django provides the escape hatch, it doesn't prevent you from using it dangerously.

What I changed in my project

I audited every place I was touching the database and made sure I was never concatenating user input into a query string. For the search, I replaced my raw query with:

from django.db.models import Q
def search(request):
    query = request.GET.get('q', '')
    posts = Post.objects.filter(
        Q(title__icontains=query) | Q(content_post__icontains=query)
    )
    return render(request, 'search.html', {'posts': posts, 'query': query})

Cleaner, safer, and more readable.

Takeaway

SQL injection is the oldest vulnerability in the OWASP Top 10 for a reason, it still works, and it still gets people. Django's ORM is good protection, but only if you let it do its job. Before your next commit, grep your codebase for .raw( and cursor.execute(. If you find any, check every one for string interpolation. The framework can't protect you from yourself.


Part of a series on the OWASP Top 10, written as I learn it, not after.

0 likes — connecte-toi pour liker.

Commentaires (0)

Connecte-toi pour commenter.

Aucun commentaire pour l'instant.