
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
Write-up : https://abraxaslabs.tech/research/cakephp-window-sqli
Classe : SQLi Portée : Distante
CakePHP 5.4.2 (887b641) - Cake Software Foundation
CVE-2026-79752 a fermé cast / extract / dateAdd de FunctionsBuilder avec ensureSimpleString(). WindowExpression::range() et frame() n'étaient pas dans ce correctif. rows() et groups() prennent ?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 liaison 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 |
| Labo | 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 labo 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 labo. 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 labo est en compilation seule |
| Changer 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 labo a utilisé range() |
Casser rows() / groups() de la même manière | Typé ?int | Fermé | Prouvé fermé en labo. rows(3) donne ROWS BETWEEN 3 PRECEDING, sans guillemets, sans liaison |
Fermé sur ce pin : cast / extract / dateAdd / jsonValue de FunctionsBuilder (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 chaîne de requête. Le reste est l'API de fenêtre documentée. Pas de RCE non authentifiée sur une application squelette. Pas de shell.
Mêmes restes d'épingles : collate SQLi, attachment CRLF, cache unserialize.
J'ai épinglé cakephp/cakephp 5.4.2 après les GHSAs de SQLi FunctionsBuilder de 2026. Dix chasses sur ce tag. CSRF / FormProtection, LFI de View, XXE de BodyParser, mass assignment, XSS de helper, et clientIp() sans trustProxy sont revenus vides dans cette barre. WindowExpressionTest::testRange affirme toujours :
$w = (new WindowExpression())->range("'1 day'", "'10 days'");
buildOffsetSql ne lie jamais un offset de 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à une ExpressionInterface.
Fausses pistes déjà consignées : traiter rows() comme la faille (il est typé int) ; affirmer que le cakephp/app standard expose cela sur un formulaire public ; exécuter le SQL injecté dans SQLite comme oracle (POC-READY est uniquement à la compilation) ; un reverse shell. Du théâtre.
python3 cakephp-window-sqli-Abraxas-Labs.py
ou
cd lab
./run.sh
Ciblez uniquement http://127.0.0.1:19440. Petite application PHP, cakephp/database 5.4.2, SQLite :memory:. GET /?mode=range&start= est passé sous forme de 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
IOC compilés en SQL :
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
Liez les décalages numériques. Autorisez par liste blanche le type de cadre (RANGE / ROWS / GROUPS) et la direction (PRECEDING / FOLLOWING). Pour les chaînes d'intervalle, mettez-les entre guillemets ou exigez ExpressionInterface. Ajoutez un test vérifiant qu'un décalage range() non entier 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. Laboratoire en boucle locale uniquement. Aucune garantie.