
CakePHP 5.4.2 - Élevé - À distance - SQLi - Décalages de plage/frame de WindowExpression injectés dans le SQL
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · cakephp-window-sqli
Classe : SQLi Portée : Distante
CakePHP 5.4.2 (887b641) - Cake Software Foundation
CVE-2026-79752 a fermé FunctionsBuilder cast / extract / dateAdd avec ensureSimpleString(). WindowExpression::range() et frame() n'étaient pas dans ce correctif. rows() et groups() prennent un ?int. range() prend toujours une chaîne. buildOffsetSql concatène l'offset, le type de frame et PRECEDING / FOLLOWING dans OVER. Le binder n'est pas utilisé sauf si l'offset est déjà un ExpressionInterface.
Une application qui transmet des données de requête dans func()->count('*')->over()->range($start, $end) place la chaîne de l'appelant dans RANGE BETWEEN sans binding ni guillemets.
| ID | pas encore de CVE |
| Classe | SQLi |
| Portée | Distante (application qui transmet des données de requête dans range() / frame()) |
| CWE | CWE-89 |
| CVSS | Élevé : 8.1 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N |
| Produit | CakePHP (cakephp/database) |
| Affecté | 5.4.2 (887b641de9f7aba06071f98f17edcbc573cbcf63) |
| Auth | application qui transmet des données de requête/formulaire dans ->over()->range($start, $end) / frame() |
| Licence | GNU Affero GPL v3.0 |
| Lab | 127.0.0.1 uniquement |
Un utilisateur capable de définir la borne de fenêtre transmise par l'application (analytique « N derniers jours », un générateur de rapports) transforme la clause OVER en SQL supplémentaire.
| Privilège | Méthode / sink | Type | Prouvé en lab ou déduit |
|---|---|---|---|
Injecter un fragment SQL dans OVER (RANGE BETWEEN ...) | GET /?mode=range&start= dans AggregateExpression::range($start, $end) / WindowExpression::frame() | Intégrité du SQL compilé | Prouvé en lab. Le SQL compilé est RANGE BETWEEN 1) /* CAKEPHP-WINDOW-SQLI-WITNESS */ -- PRECEDING avec un binder vide |
| Lire ou modifier des lignes en tant qu'utilisateur DB de l'application | Même requête, exécutée | Intégrité / reconnaissance de la DB | Déduit de l'injection. Ce lab est en compilation seule |
| Modifier le type de frame ou la direction de la même manière | frame($type, $offset, $direction, ...) concatène aussi ces chaînes | Intégrité du SQL compilé | Déduit de sql() / buildOffsetSql(). Le lab a utilisé range() |
Casser rows() / groups() de la même manière | Typé ?int | Fermé | Prouvé fermé en lab. rows(3) donne ROWS BETWEEN 3 PRECEDING, sans guillemets, sans binding |
Fermé sur ce pin : FunctionsBuilder cast / extract / dateAdd / jsonValue (CVE-2026-79752 / CVE-2026-77635) ; Query::limit / offset (int typé). Le cakephp/app par défaut n'appelle pas range() avec des données de query-string. Le reste est l'API window documentée. Pas de RCE non authentifiée sur une application squelette. Pas de shell.
Autres restes sur le même pin : collate SQLi, attachment CRLF, cache unserialize.
J'ai épinglé cakephp/cakephp 5.4.2 après les GHSAs SQLi de FunctionsBuilder de 2026. Dix recherches sur ce tag. CSRF / FormProtection, View LFI, BodyParser XXE, mass assignment, helper XSS et clientIp() sans trustProxy sont revenus vides dans ce barème. WindowExpressionTest::testRange affirme toujours :
$w = (new WindowExpression())->range("'1 day'", "'10 days'");
buildOffsetSql ne lie jamais un offset de type chaîne :
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 n'est pas utilisé sauf si l'offset est déjà un ExpressionInterface.
Fausses pistes déjà consignées : traiter rows() comme la faille (il est typé int) ; prétendre que le cakephp/app standard expose cela sur un formulaire public ; exécuter le SQL injecté dans SQLite comme oracle (POC-READY est en compilation seule) ; un reverse shell. Du théâtre.
python3 cakephp-window-sqli-Abraxas-Labs.py
ou
cd lab
./run.sh
Cible uniquement http://127.0.0.1:19440. Petite application PHP, cakephp/database 5.4.2, SQLite :memory:. GET /?mode=range&start= est passé comme chaîne dans range(). GET /?mode=rows&n=3 est le contrôle int.
SUCCESS cakephp-window-sqli pin=5.4.2 range-raw=yes rows-numeric=yes bound=no quoted=no CAKEPHP-WINDOW-SQLI-WITNESS
SQL compilé IOC :
SELECT (COUNT(*) OVER (RANGE BETWEEN 1) /* CAKEPHP-WINDOW-SQLI-WITNESS */ -- PRECEDING AND CURRENT ROW)) AS n FROM sqlite_master
Contrôle :
SELECT (COUNT(*) OVER (ROWS BETWEEN 3 PRECEDING AND CURRENT ROW)) AS n FROM sqlite_master
Lier les offsets numériques. Autoriser par liste blanche le type de frame (RANGE / ROWS / GROUPS) et la direction (PRECEDING / FOLLOWING). Pour les chaînes d'intervalle, les mettre entre guillemets ou exiger un ExpressionInterface. Ajouter un test vérifiant qu'un offset range() non-int devient un placeholder ou un littéral entre guillemets, et non du SQL brut.
887b641de9f7aba06071f98f17edcbc573cbcf63)WindowExpression.php · AggregateExpression.phpGNU Affero GPL v3.0. Voir LICENSE. Lab en loopback uniquement. Aucune garantie.