Feb 9, 2026·5 min read·15 visits
Authenticated attackers can turn the Craft CMS Control Panel into a SQL playground. By sending a crafted JSON payload to the element index endpoint, you can inject raw SQL into the 'ORDER BY' clause. This happens because the code blindly applies user input to the database query builder object.
A high-severity SQL injection vulnerability in Craft CMS allows authenticated Control Panel users to execute arbitrary SQL commands. The flaw stems from a mass assignment vulnerability where user-supplied JSON keys are directly mapped to query builder methods, specifically bypassing sanitization mechanisms in the underlying Yii2 framework.
We often treat the "Admin Panel" or "Control Panel" as a sanctuary—a place where only trusted individuals tread, and where input validation can arguably be a little looser. This assumption is, to put it mildly, dead wrong. CVE-2026-25495 is a perfect reminder that "Authenticated" does not mean "Trusted," and that even low-privileged content editors can be just as dangerous as external threat actors if the code permits it.
In the world of Craft CMS, the element-indexes/get-elements endpoint is the workhorse responsible for fetching lists of entries, users, or assets to display in the UI. It’s a dynamic, heavy-lifting component that needs to be flexible. Unfortunately, in its quest for flexibility, it committed one of the cardinal sins of secure coding: it allowed the user to define the structure of the database query directly via a JSON object.
This isn't just a simple case of a missing quote in a SQL string. It's a logic flaw involving "Mass Assignment"—a pattern where an application takes a pile of user input and blindly maps it to internal object properties. In this case, that internal object was a loaded gun pointed directly at the database.
The root cause lies in how Craft CMS leverages the Yii2 framework's "magic" configuration methods. Specifically, the code used Craft::configure($query, $criteria) to apply filters received from the frontend directly to the ElementQuery object. Think of Craft::configure as a function that says, "For every key in this array, find the corresponding property or setter method on the object and update it."
Here is the kicker: ElementQuery inherits from Yii2's Query Builder. This means it has public methods like orderBy(), where(), join(), and union(). When a user sends a POST request with a JSON body containing "criteria": {"orderBy": ...}, the application dutifully executes $query->orderBy(...) with the attacker's data.
But wait, doesn't Yii2 escape inputs? Usually, yes. However, Yii2 has a specific behavior for the orderBy method. If the string passed to it contains parentheses—for example, (id)—Yii2 assumes the developer is trying to use a raw SQL expression (like a database function) and disables escaping for that segment. This is a feature, not a bug, in the framework. But when you expose that feature directly to user input, it becomes a catastrophic vulnerability.
Let's look at the crime scene in src/controllers/ElementIndexesController.php. The vulnerability is concise and elegant in its negligence.
The Vulnerable Code:
// Get the untrusted input from the request body
$criteria = Craft::$app->getRequest()->getBodyParam('criteria');
// ... contextual setup ...
// FATAL ERROR: Blindly apply input to the Query object
Craft::configure($query, Component::cleanseConfig($criteria));The cleanseConfig method sounds safe, right? It's not. It mostly handles Project Config compatibility and doesn't sanitize against SQL injection vectors. It essentially allows the attacker to drive the ElementQuery object.
The Fix (Commit 96c60d775c644ff0a0276da52fe29e11d4cd38d2): The patch implements a blacklist approach (which is usually frowned upon, but necessary here given the architecture). They explicitly unset the dangerous methods from the input array before configuring the object.
// The "No-Fly" List
unset(
$criteria['where'],
$criteria['orderBy'], // The main culprit
$criteria['select'],
$criteria['join'],
$criteria['union'],
// ... and others
);
// Now it is safe(r) to configure
Craft::configure($query, Component::cleanseConfig($criteria));By manually stripping out keys that correspond to structural query modifications, the developers ensure that user input can only influence safe properties (like limit or offset), effectively closing the open window.
So, how do we weaponize this? We know we can call orderBy. We know that Yii2 will escape standard strings like id ASC. To inject SQL, we need to trigger the "Raw Expression" mode by using parentheses.
The Setup:
element-indexes/get-elements.The Payload:
{
"action": "element-indexes/get-elements",
"criteria": {
"orderBy": "(elements.id), (SELECT SLEEP(5))"
}
}Why this works:
When Yii2 receives (elements.id), (SELECT SLEEP(5)), it sees the parentheses and thinks, "Ah, the developer is doing something complex with DB functions. I better not touch this with my quoting logic." The resulting SQL generated by the backend looks something like this:
SELECT `elements`.*
FROM `elements`
...
ORDER BY (elements.id), (SELECT SLEEP(5))Since SLEEP(5) is a valid expression in MySQL, the database pauses for 5 seconds. This confirms we have Time-Based Blind SQL Injection. From here, an attacker can use standard techniques to extract the database version, current user, and eventually the users table to grab the admin's bcrypt hash.
While this requires authentication, don't let that lower your blood pressure. In many organizations, "Content Editor" or "Intern" accounts are shared, protected by weak passwords, or lack 2FA. Once an attacker compromises a low-level account, this vulnerability allows them to pivot to full system control.
With SQL Injection in Craft CMS, the attacker can:
sessions table (if stored in DB) or the Admin password hash, crack it, and log in as Super Admin.Essentially, this is a "Game Over" vulnerability. The barrier to entry is low, and the ceiling for impact is the roof.
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Craft CMS Pixel & Tonic | >= 4.0.0-RC1, <= 4.16.17 | 4.16.18 |
Craft CMS Pixel & Tonic | >= 5.0.0-RC1, <= 5.8.21 | 5.8.22 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-89 |
| CVSS v4.0 | 8.7 (High) |
| Attack Vector | Network (Authenticated) |
| Exploit Status | PoC Available |
| Vulnerability Type | SQL Injection via Mass Assignment |
| Affected Component | ElementIndexesController (get-elements) |