
CakePHP 5.4.2 - High - Remote - SQLi - WindowExpression range/frame offsets spliced into SQL
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · cakephp-window-sqli
Class: SQLi Reach: Remote
CakePHP 5.4.2 (887b641) - Cake Software Foundation
CVE-2026-79752 closed FunctionsBuilder cast / extract / dateAdd with ensureSimpleString(). WindowExpression::range() and frame() were not in that fix. rows() and groups() take ?int. range() still takes a string. buildOffsetSql concatenates the offset, frame type, and PRECEDING / FOLLOWING into OVER. The binder is unused unless the offset is already an ExpressionInterface.
An app that forwards request data into func()->count('*')->over()->range($start, $end) puts the caller's string into RANGE BETWEEN unbound and unquoted.
| ID | no CVE yet |
| Class | SQLi |
| Reach | Remote (app that forwards request data into range() / frame()) |
| CWE | CWE-89 |
| CVSS | High: 8.1 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N |
| Product | CakePHP (cakephp/database) |
| Affected | 5.4.2 (887b641de9f7aba06071f98f17edcbc573cbcf63) |
| Auth | application that forwards query/form data into ->over()->range($start, $end) / frame() |
| License | GNU Affero GPL v3.0 |
| Lab | 127.0.0.1 only |
A user who can set the window bound the app forwards (analytics "last N days", a report builder) changes the OVER clause into extra SQL.
| Privilege | Method / sink | Kind | Lab-proven or inferred |
|---|---|---|---|
Splice a SQL fragment into OVER (RANGE BETWEEN ...) | GET /?mode=range&start= into AggregateExpression::range($start, $end) / WindowExpression::frame() | Integrity of compiled SQL | Lab-proven. Compiled SQL is RANGE BETWEEN 1) /* CAKEPHP-WINDOW-SQLI-WITNESS */ -- PRECEDING with an empty binder |
| Read or modify rows as the app's DB user | Same query, executed | Integrity / recon of DB | Inferred from the splice. This lab is compile-only |
| Change frame type or direction the same way | frame($type, $offset, $direction, ...) concatenates those strings too | Integrity of compiled SQL | Inferred from sql() / buildOffsetSql(). Lab used range() |
Break rows() / groups() the same way | Typed ?int | Closed | Lab-proven closed. rows(3) is ROWS BETWEEN 3 PRECEDING, not quoted, not bound |
Closed on this pin: FunctionsBuilder cast / extract / dateAdd / jsonValue (CVE-2026-79752 / CVE-2026-77635); Query::limit / offset (typed int). Default cakephp/app does not call range() with query-string data. The leftover is the documented window API. Not unauthenticated RCE on a skeleton app. Not a shell.
Same pin leftovers: collate SQLi, attachment CRLF, cache unserialize.
I pinned cakephp/cakephp 5.4.2 after the 2026 FunctionsBuilder SQLi GHSAs. Ten hunts on that tag. CSRF / FormProtection, View LFI, BodyParser XXE, mass assignment, helper XSS, and clientIp() without trustProxy came back empty in this bar. WindowExpressionTest::testRange still asserts:
$w = (new WindowExpression())->range("'1 day'", "'10 days'");
buildOffsetSql never binds a string offset:
protected function buildOffsetSql(
ValueBinder $binder,
ExpressionInterface|string|int|null $offset,
string $direction,
): string {
if ($offset === 0) {
return 'CURRENT ROW';
}
if ($offset instanceof ExpressionInterface) {
$offset = $offset->sql($binder);
}
return sprintf('%s %s', $offset ?? 'UNBOUNDED', $direction);
}
$binder is unused unless the offset is already an ExpressionInterface.
Wrong turns already recorded: treating rows() as the hole (it is typed int); claiming stock cakephp/app exposes this on a public form; executing the injected SQL in SQLite as the oracle (POC-READY is compile-only); a reverse shell. Theatre.
python3 cakephp-window-sqli-Abraxas-Labs.py
or
cd lab
./run.sh
Target only http://127.0.0.1:19440. Tiny PHP app, cakephp/database 5.4.2, SQLite :memory:. GET /?mode=range&start= is passed as a string into range(). GET /?mode=rows&n=3 is the int control.
SUCCESS cakephp-window-sqli pin=5.4.2 range-raw=yes rows-numeric=yes bound=no quoted=no CAKEPHP-WINDOW-SQLI-WITNESS
IOC compiled SQL:
SELECT (COUNT(*) OVER (RANGE BETWEEN 1) /* CAKEPHP-WINDOW-SQLI-WITNESS */ -- PRECEDING AND CURRENT ROW)) AS n FROM sqlite_master
Control:
SELECT (COUNT(*) OVER (ROWS BETWEEN 3 PRECEDING AND CURRENT ROW)) AS n FROM sqlite_master
Bind numeric offsets. Allowlist frame type (RANGE / ROWS / GROUPS) and direction (PRECEDING / FOLLOWING). For interval strings, quote them or require ExpressionInterface. Add a test that a non-int range() offset becomes a placeholder or a quoted literal, not raw SQL.
887b641de9f7aba06071f98f17edcbc573cbcf63)WindowExpression.php · AggregateExpression.phpGNU Affero GPL v3.0. See LICENSE. Loopback lab only. No warranty.