Django CVE-2025-64459: ORM Query Injection Explained
Hey there! Today we’re diving into an interesting Django vulnerability - CVE-2025-64459. This is a fascinating example of how convenience features in web frameworks can sometimes become security risks when not handled carefully.
Task 1: Introduction
Django is a widely used Python web framework that exposes Object-Relational Mapping (ORM) layer to let developers query databases using familiar Python syntax. That convenience becomes a liability when user input is fed straight into ORM calls without validation.
In particular, some applications expand request parameters dictionaries directly into filter(), exclude() or get() calls. For example, filter(**request.GET.dict()) inadvertently allows an attacker to supply internal query-control parameters.
A critical vulnerability exists where specially crafted query parameters such as _connector and _negated can be injected into those ORM calls. By manipulating how query clauses are combined or inverted, an attacker can alter the logical structure or database queries leading to unauthorized data access, authentication bypasses, or privilege escalation.
The issue is high-severity (CVSS ~9.1) and straightforward to exploit in many common coding patterns.
This room is explaining what makes the flaw possible, show how to identify the risky code pattern in a codebase, demonstrate practical exploitation in an isolated lab environments, and walk through remediation and detection strategies so the same mistake is not repeated in production.
What Makes This Vulnerability Tick?
Django’s Object-Relational Mapping (ORM) is pretty awesome - it lets us write database queries using Python instead of raw SQL. You can do things like User.objects.filter(username='admin') and Django handles the SQL generation for you.
But here’s where things get interesting (and dangerous). Some developers take shortcuts by passing user input directly into these ORM calls. Something like User.objects.filter(**request.GET.dict()) might seem convenient, but it opens the door for attackers to inject special Django parameters.
The vulnerability centers around two internal parameters that weren’t properly protected:
_connector: Controls how query conditions are joined (AND/OR logic)_negated: Flips the meaning of a query (turns filters upside down)
Before the patch, attackers could manipulate these to completely change how database queries worked. Imagine turning a strict login check into an OR condition that always returns true - yikes!
Time to Get Our Hands Dirty!
Let’s see this vulnerability in action. Here’s the vulnerable code we’re working with:
def post_list(request): query_params = dict(request.GET.items()) if not any(param.startswith('is_published') for param in query_params.keys()): query_params['is_published'] = True if not any(param.startswith('id') for param in query_params.keys()): query_params['id__lt'] = 10 q_filter = Q(**query_params) posts = Post.objects.filter(q_filter)Notice that Q(**query_params) line? That’s our entry point!
Finding Posts the Normal Way
First, let’s try the intended functionality. If we want to see posts by “Security Architect”, we’d visit:
http://10.66.148.200:8000/poc/?author=Security%20Architect
This works as expected and shows us just one post.
Breaking Things (Responsibly!)
Now for the fun part. We can exploit this by manipulating the _connector parameter. By setting it to OR 1=1 OR, we create a condition that always evaluates to true, giving us access to ALL posts in the database:
http://10.10.148.200:8000/poc/?author=Security%20Architect&_connector=OR%201=1%20OR
Boom! Now we can see posts from all authors, including potentially sensitive ones that weren’t meant to be public.
Challenge Questions & Solutions
Question 1: What is the title of the post by “DevOps Engineer”?
To find this, we need to use our exploitation technique to reveal all posts in the database. Using the URL:
http://10.66.148.200:8000/poc/?author=Security%20Architect&_connector=OR%201=1%20OR
Answer: Monitoring Django Apps - Advanced
Question 2: Browse to the employees page. What is the name of the employee whose hire date is “June 5, 2022”?
Navigate to http://10.66.148.200:8000/poc/employees and use similar exploitation techniques.
Answer: David Rodriguez
What We’ve Learned
This vulnerability is a great reminder that security isn’t just about complex exploits - sometimes the most dangerous vulnerabilities come from seemingly innocent shortcuts in our code.
Key Takeaways:
- Never trust user input: Always validate and sanitize data before using it in database queries
- Whitelist over blacklist: Instead of trying to block bad parameters, only allow known good ones
- Keep things updated: Django’s security team fixed this quickly - make sure you’re running patched versions
- Code reviews matter: A second pair of eyes might catch these patterns before they hit production
The Django team has since fixed this by preventing _connector and _negated from being accepted through external input. But the lesson remains: convenience features can become security liabilities if we’re not careful.
Stay curious, stay secure! 🔒
Next reads
View all →25 Dec
n8n CVE-2025-68613: When Workflow Automation Goes Wrong
A comprehensive look at CVE-2025-68613 in n8n - how expression injection can lead to remote code execution and what you can do to protect yourself.
15 Dec
TryHackMe: Passwords - A Cracking Christmas
A comprehensive walkthrough of the TryHackMe Passwords room, exploring password-based encryption attacks and defensive strategies during the holiday season.
10 May
Copy Fail: CVE-2026-31431 (TryHackMe)
Exploit copy-fail, a kernel LPE that corrupts any file's page cache to gain root in seconds.
Get posts by email
One email when I publish, not a drip, not weekly. Sign up and I'll only write when there's something new.
You won't get mail just for signing up. Unsubscribe any time.