
CVE-2020-24932: A Sorting Parameter, a Missing Check, and a Critical SQLi
Opening thesis — why a sort parameter can become a SQL injection path
I do not treat a sort dropdown as a harmless UI detail.
Once the backend turns a sort field into SQL syntax, that field stops being presentation metadata and starts becoming part of the query language. If the server also skips a permission check before it builds the query, the client is no longer choosing how to display data; it is steering how the database is asked to shape it.
That is the practical lesson I draw from CVE-2020-24932. The public write-up frames it as a critical SQL injection tied to a sorting parameter and a missing check. That combination matters because it is easy to underestimate: the input looks small, the feature looks read-only, and the code often lives in a shared helper that nobody treats as sensitive.
Prepared statements usually do not parameterize identifiers, sort directions, or raw ORDER BY fragments. If the bug is in SQL grammar, the fix is usually an allowlist and a server-side mapping, not escaping.
The narrow bug class hidden inside a normal UI feature
The narrow bug class here is not “bad input got through.”
It is “a normal UI choice was promoted into SQL grammar without a server-owned contract.”
That distinction matters. A free-text search box can be safe if it is bound as a value. A sort field can be dangerous even when it only accepts a few visible options, because the server may still interpolate it directly into ORDER BY, LIMIT, ASC, DESC, or a raw expression helper. Once that happens, the attacker is no longer trying to inject a string into data. They are trying to influence the query structure itself.
Sorting features are especially easy to get wrong because they feel deterministic. Developers assume the UI already limited the choices, or that the query builder is “just ordering rows,” not opening a new attack surface.
Clear position: the critical failure was query construction, not just bad input
My view is simple: the critical failure in a case like CVE-2020-24932 is the query construction path, not the presence of user input by itself.
If the backend accepts a request, checks authorization, maps the requested sort to a fixed server-side fragment, and then executes the query with bound values, the input is mostly harmless. If it skips the server-side mapping and glues raw request text into the SQL string, the bug is already there before any payload is tried.
That is why I rank this class as a trust-boundary failure first and a sanitization failure second. Sanitization can be part of the cleanup, but it is not the design fix.
What the public reports say about CVE-2020-24932
Confirmed facts from the advisory and later write-up
From the public material I could verify, the core facts are modest but important:
- CVE-2020-24932 is described as a critical SQL injection issue.
- The vulnerable surface involves a sorting parameter.
- The write-up also points to a missing check, which suggests the backend trusted the request path more than it should have.
- The issue is framed as a security problem in ordinary application flow, not a low-level database bug.
That is enough to justify a defensive review even without the full exploit chain. A sort parameter on its own is not novel. A sort parameter that can affect SQL syntax is a real issue. A missing check before query generation is what turns it from a nuisance into a security bug.
What the reports do not prove and should be treated as inference
The public snippet does not prove several details, and I do not want to smuggle those in as facts:
- the exact product or module involved
- the exact SQL fragment that was concatenated
- whether the exploit was boolean-based, time-based, union-based, or error-based
- whether stacked queries were possible in the affected deployment
- the exact version range or patch commit
Those details may exist in the original advisory or write-up, but they are not visible in the source seed I was given. So when I talk about mechanics below, I am describing the bug class and the safe lab pattern, not claiming I reproduced the original target.
The request path from browser input to database query
How sort, direction, or ordering fields tend to reach SQL
The vulnerable path usually looks boring until you diagram it.
- The browser sends a query parameter such as
sort,order,direction, ororderby. - A controller copies that request value into a variable with little or no validation.
- A query helper builds an
ORDER BYclause or a related fragment. - The database executes the final string.
The dangerous part is the third step. Values like title or created_at are not the same thing as SQL identifiers. DESC is not the same thing as a data value. If the code accepts those fragments as text, it is no longer binding data; it is building syntax.
Here is the pattern I look for in code review:
| Field | Looks harmless | What it becomes if concatenated |
|---|---|---|
sort | UI preference | column name or SQL expression |
direction | enum-like choice | ASC / DESC keyword |
filter | search text | WHERE condition |
page / limit | pagination | LIMIT / OFFSET clause |
group | display mode | GROUP BY or join selection |
The biggest mistake is assuming that “small” request parameters are safer than large ones. Small fields are often more dangerous because they are treated as structure, not content.
Where a missing authorization check turns a UI choice into a trust failure
The “missing check” part is what upgrades this from a pure injection bug to a trust-boundary failure.
If a route serves user-visible data, the backend should first decide what the caller is allowed to see. Only after that should it decide how to sort or present the result set. If the check happens after query generation, or if the query itself encodes access assumptions, the code is backward.
I usually think of it this way:
- authorization answers: may this caller query this data set?
- sorting answers: how should already-authorized rows be ordered?
Those two concerns should not be mixed. If the client can influence the structure of the query before the authorization gate closes, the UI has become a control plane.
Rebuilding the flaw in a safe lab
Minimal test harness, redacted inputs, and expected environment
You do not need the original product to understand the flaw. A toy harness is enough to show why raw sort fragments are dangerous.
This example uses Node.js and SQLite in memory. It is intentionally small and local.
npm init -y
npm install express better-sqlite3
node server.js
A minimal server might look like this:
const express = require("express");
const Database = require("better-sqlite3");
const db = new Database(":memory:");
db.exec(`
CREATE TABLE items (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
created_at TEXT NOT NULL
);
INSERT INTO items (title, created_at) VALUES
('alpha', '2024-01-01'),
('beta', '2024-01-02');
`);
const app = express();
app.get("/items", (req, res) => {
const sort = req.query.sort || "created_at";
const sql = `SELECT id, title, created_at FROM items ORDER BY ${sort}`;
console.log(sql);
res.json(db.prepare(sql).all());
});
app.listen(3000, () => console.log("listening on 3000"));
This is unsafe by design. That is the point. The query is interpolating a request value directly into SQL grammar.
A harmless payload to confirm SQL parsing without causing damage
A destructive payload is not necessary to prove the bug.
In a safe lab, I only need a payload that changes the query shape or row order. For example:
curl 'http://localhost:3000/items?sort=created_at'
curl 'http://localhost:3000/items?sort=created_at%20DESC'
The first request should return rows in ascending order:
[
{"id":1,"title":"alpha","created_at":"2024-01-01"},
{"id":2,"title":"beta","created_at":"2024-01-02"}
]
The second should reverse the order:
[
{"id":2,"title":"beta","created_at":"2024-01-02"},
{"id":1,"title":"alpha","created_at":"2024-01-01"}
]
That is enough to confirm that the server is accepting raw SQL structure from the request. No error message, union select, or multi-statement trick is required to make the point.
If you want one more harmless confirmation, add a logging line and watch the exact SQL string:
SELECT id, title, created_at FROM items ORDER BY created_at DESC
When you see the request value appear in the final SQL text, the trust failure is already visible.
Observed responses that distinguish normal sorting from injection
In practice, the difference between a safe implementation and a vulnerable one shows up quickly:
| Request | Safe backend | Unsafe backend |
|---|---|---|
?sort=created_at | accepted only if allowlisted | accepted and interpolated |
?sort=created_at DESC | rejected or mapped | changes SQL order |
?sort=created_at DESC, id DESC | rejected | often still executes as SQL syntax |
| invalid value | 400 or fallback | syntax error, odd sort, or unexpected query |
The exact error response depends on the database and driver. That variability is why I prefer looking at the SQL log and the result order together. If the log changes with client input, the issue is structural.
Reading the vulnerable code with an attacker’s eye
Query assembly patterns to search for
When I audit code for this bug class, I search for a few patterns first:
- string concatenation near
ORDER BY - helpers named
sort,orderBy,buildQuery,buildSql - raw SQL escape hatches such as
raw,literal, orunsafe - request parameters copied directly into a query string
- conditionals that switch between SQL fragments based on client input
A suspicious fragment often looks like this:
const sql = `
SELECT id, title, created_at
FROM items
WHERE owner_id = ${userId}
ORDER BY ${req.query.sort} ${req.query.direction}
`;
There are two separate bugs here:
owner_idis interpolated instead of bound.sortanddirectionare treated as trusted SQL syntax.
The second bug is the more interesting one for CVE-2020-24932-style issues, because it can hide inside code that otherwise looks “mostly safe.”
Why allowlists beat sanitization for structural SQL fragments
For structural SQL fragments, sanitization is the wrong mental model.
You do not want to “clean up” arbitrary text until it happens to look like a column name. You want to accept only a known set of server-owned values and map them to prewritten SQL fragments.
Good:
const SORT_MAP = {
newest: "created_at DESC",
oldest: "created_at ASC",
title: "title COLLATE NOCASE ASC",
};
const sortKey = req.query.sort || "newest";
const orderBy = SORT_MAP[sortKey];
if (!orderBy) {
return res.status(400).json({ error: "invalid sort" });
}
const sql = `SELECT id, title, created_at FROM items ORDER BY ${orderBy}`;
Better still, if your query builder supports it, keep the raw fragment fully server-owned and bind only values.
The key idea is that the client should choose from a semantic menu, not from query text. The server translates that menu into SQL it already knows is safe.
The backend check that should have preceded query generation
The missing check should come before the query is even assembled.
A safe flow looks like this:
- Identify the caller.
- Verify the caller may access this data set.
- Choose an allowlisted sort key.
- Map that key to a server-owned SQL fragment.
- Execute the query with bound values.
A dangerous flow looks like this:
- Read request parameters.
- Build SQL text.
- Execute it.
- Maybe reject something later.
That ordering is backwards. Once the query text has been built from hostile input, the security boundary is already weakened. If the route also depends on role or subscription state, the authorization decision must be part of the data access layer, not a UI afterthought.
Impact analysis: why the vulnerability was rated critical
Data disclosure, privilege bypass, and possible query shaping
A critical SQL injection usually earns that label because the impact can extend beyond one page of results.
Depending on the database permissions and query shape, a successful attacker may be able to:
- read rows beyond what the UI intended to expose
- alter query logic enough to bypass filtering
- infer data through ordering, errors, or timing
- reach other tables if the app account is too broad
- in some environments, chain the bug with other weaknesses for bigger impact
I want to be precise here: whether write access, stacked queries, or broader database compromise is possible depends on the driver, DB configuration, and privilege model. I did not verify those conditions for the original CVE. What is confirmed from the public description is that the injection was considered critical, which is already a strong signal that the bug was not cosmetic.
Why sorting endpoints are often underestimated attack surface
Sorting endpoints get ignored for predictable reasons:
- they are read-oriented, so they feel safer than form submissions
- they often have few visible options, which gives a false sense of control
- they are implemented in shared query helpers, so the risky code is hidden
- test coverage focuses on visible content, not on SQL grammar
In my experience, that combination is why these bugs survive code review. Everyone checks the search box. Nobody checks the little sort dropdown that the UI copied into a SQL fragment six layers down.
Fixes that hold up under testing
Move authorization into the server-side data access layer
The first fix is architectural.
Do not let route handlers decide access in one branch and data access in another. Put the permission check where the data query is built or invoked, so unauthorized callers cannot influence the query shape before the check passes.
If a route can only show rows for the current tenant, make that tenant filter part of the data access function. If the caller cannot see admin-only fields, do not even include them as selectable sort keys.
That is how you prevent the UI from becoming a policy engine.
Replace free-form ordering with strict enumerated values
The sort request should be a semantic token, not SQL text.
A clean pattern is:
const SORT_MAP = {
newest: "created_at DESC",
oldest: "created_at ASC",
title: "title ASC",
};
function resolveSort(key) {
const orderBy = SORT_MAP[key];
if (!orderBy) throw new Error("invalid sort");
return orderBy;
}
Then test that invalid values fail closed:
curl 'http://localhost:3000/items?sort=not-a-real-sort'
Expected behavior:
- HTTP 400
- no SQL execution with the raw input
- consistent error body
- no fallback to a dangerous default
A silent fallback can be dangerous if it hides attacker probing.
Use parameterized queries where possible and reject SQL fragments elsewhere
Use binds for values:
const rows = db.prepare(
"SELECT id, title FROM items WHERE owner_id = ?"
).all(userId);
Do not pretend binds can solve identifiers or ORDER BY keywords. They usually cannot.
If you need dynamic ordering, map allowed options to static fragments. If you need dynamic filters, bind the values and keep the operators fixed in code. If you absolutely must use a raw SQL helper, confine it to server-owned strings that never contain request text.
Add regression tests, negative cases, and security logging
I would add three kinds of tests:
-
Allowed sort values
newest,oldest,title- ensure each returns a valid response
-
Rejected values
created_at DESCid, created_atfoo bar- ensure each returns 400 or a safe validation error
-
Authorization cases
- user can sort only within their own dataset
- user cannot access hidden columns or hidden row sets
- rejected inputs never reach the DB layer
A compact test table is usually enough to keep this from regressing:
| Case | Expected result |
|---|---|
| known sort token | 200, correct order |
| unknown sort token | 400, no query execution |
| unauthorized user | 403, no data access |
| malformed fragment | 400, no SQL syntax leak |
Logging also matters. If your app receives repeated invalid sort values, that is often probing. Log the rejection at the boundary, but do not echo raw SQL errors back to the client.
A practical audit checklist for similar bugs
Search for order-by, sort, filter, and direction parameters
Start with the obvious request names:
sortorderorderbydirectionfieldcolumnfiltergroup
Then trace each one into query builders, ORM raw helpers, and controller code. If a parameter ever becomes part of SQL text instead of a bound value or allowlisted enum, you have found a candidate issue.
Grep for string concatenation near database calls and permission checks
I look for a few code smells:
- template literals with query keywords nearby
+ req.query.* +raw(...),literal(...),unsafe(...)- code that builds a query before checking role or tenant
- helpers that accept a string and “normalize” it without mapping it
A simple grep can surface a surprising amount:
grep -RInE 'ORDER BY|sort|orderBy|raw\(|literal\(|unsafe' src/
That will not prove a vulnerability, but it will find the seams where one often hides.
Verify that rejected inputs fail closed and produce consistent output
For this bug class, I want to see one of three outcomes:
- the input is mapped to a fixed fragment
- the input is rejected before any query is built
- the input is ignored and replaced with a safe server default
What I do not want is:
- raw SQL syntax in logs
- different error shapes for different malformed values
- fallback behavior that still touches the database
- any route where client text becomes query grammar
That is the point where a sort feature turns into an attack surface.
What I confirmed versus what remains unverified
Separating public facts from educated inference
Here is the clean split as I see it:
| Confirmed from public material | Inference or not verified here |
|---|---|
| CVE-2020-24932 is described as a critical SQL injection | exact product/module affected |
| the public write-up ties it to a sorting parameter | exact query text or vulnerable line |
| a missing check is part of the failure story | exact authorization logic that failed |
| a fix should involve server-side validation and query hardening | exploit style, payload details, and privilege impact in a specific deployment |
That separation matters more than it sounds. When a report is thin, it is easy to fill in the blanks with a favorite exploit narrative. I would rather be explicit that I am reasoning from the bug class than pretend I saw the original exploit path.
My own conclusion, after looking at this category of flaw repeatedly, is that the sort parameter is not the real problem. The real problem is that the backend let a client shape SQL grammar before the server had finished deciding what that client was allowed to do.
Conclusion — the real lesson is about trust boundaries, not just SQL syntax
The useful takeaway from CVE-2020-24932 is not “sanitize sort values.”
That advice is too small. The deeper fix is to stop treating client-controlled ordering as data and start treating it as policy. The server should decide which sorts exist, which rows are visible, and which fragments of SQL are ever allowed to be constructed.
If I were auditing a codebase for this class of bug, I would look at every sort, direction, filter, and raw query helper with suspicion. The vulnerable line is often tiny. The design mistake around it is usually larger.


