
CakePHP 5.4.2 - Alta - Remota - SQLi - Deslocamentos de intervalo/frame do WindowExpression inseridos no SQL
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · cakephp-window-sqli
Classe: SQLi Alcance: Remoto
CakePHP 5.4.2 (887b641) - Cake Software Foundation
CVE-2026-79752 fechou FunctionsBuilder cast / extract / dateAdd com ensureSimpleString(). WindowExpression::range() e frame() não estavam nessa correção. rows() e groups() aceitam ?int. range() ainda aceita uma string. buildOffsetSql concatena o offset, o tipo de frame e PRECEDING / FOLLOWING no OVER. O binder não é usado a menos que o offset já seja um ExpressionInterface.
Uma aplicação que encaminha dados da requisição para func()->count('*')->over()->range($start, $end) coloca a string do chamador em RANGE BETWEEN sem binding e sem aspas.
| ID | ainda sem CVE |
| Classe | SQLi |
| Alcance | Remoto (aplicação que encaminha dados da requisição para range() / frame()) |
| CWE | CWE-89 |
| CVSS | Alto: 8.1 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N |
| Produto | CakePHP (cakephp/database) |
| Afetado | 5.4.2 (887b641de9f7aba06071f98f17edcbc573cbcf63) |
| Autenticação | aplicação que encaminha dados de query/form para ->over()->range($start, $end) / frame() |
| Licença | GNU Affero GPL v3.0 |
| Lab | apenas 127.0.0.1 |
Um usuário que consegue definir o limite de janela que a aplicação encaminha (analytics "últimos N dias", um construtor de relatórios) transforma a cláusula OVER em SQL adicional.
| Privilégio | Método / sink | Tipo | Comprovado em lab ou inferido |
|---|---|---|---|
Inserir um fragmento SQL em OVER (RANGE BETWEEN ...) | GET /?mode=range&start= para AggregateExpression::range($start, $end) / WindowExpression::frame() | Integridade do SQL compilado | Comprovado em lab. O SQL compilado é RANGE BETWEEN 1) /* CAKEPHP-WINDOW-SQLI-WITNESS */ -- PRECEDING com um binder vazio |
| Ler ou modificar linhas como o usuário de BD da aplicação | Mesma query, executada | Integridade / reconhecimento do BD | Inferido a partir da inserção. Este lab é apenas de compilação |
| Alterar o tipo de frame ou a direção da mesma forma | frame($type, $offset, $direction, ...) também concatena essas strings | Integridade do SQL compilado | Inferido a partir de sql() / buildOffsetSql(). O lab usou range() |
Quebrar rows() / groups() da mesma forma | Tipados como ?int | Fechado | Comprovado em lab como fechado. rows(3) é ROWS BETWEEN 3 PRECEDING, sem aspas, sem binding |
Fechado neste pin: FunctionsBuilder cast / extract / dateAdd / jsonValue (CVE-2026-79752 / CVE-2026-77635); Query::limit / offset (tipados como int). O cakephp/app padrão não chama range() com dados de query-string. O que resta é a API de janela documentada. Não é RCE não autenticado em uma aplicação esqueleto. Não é um shell.
Outros resquícios do mesmo pin: collate SQLi, attachment CRLF, cache unserialize.
Eu fixei cakephp/cakephp 5.4.2 após os GHSAs de SQLi do FunctionsBuilder de 2026. Dez buscas nessa tag. CSRF / FormProtection, View LFI, BodyParser XXE, mass assignment, helper XSS e clientIp() sem trustProxy voltaram vazios nesse critério. WindowExpressionTest::testRange ainda afirma:
$w = (new WindowExpression())->range("'1 day'", "'10 days'");
buildOffsetSql nunca faz binding de um offset string:
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ão é usado a menos que o offset já seja um ExpressionInterface.
Caminhos errados já registrados: tratar rows() como a falha (ele é tipado como int); afirmar que o cakephp/app padrão expõe isso em um formulário público; executar o SQL injetado no SQLite como oráculo (POC-READY é apenas de compilação); um reverse shell. Teatro.
python3 cakephp-window-sqli-Abraxas-Labs.py
ou
cd lab
./run.sh
Alvo apenas http://127.0.0.1:19440. Aplicação PHP pequena, cakephp/database 5.4.2, SQLite :memory:. GET /?mode=range&start= é passado como string para range(). GET /?mode=rows&n=3 é o controle int.
SUCCESS cakephp-window-sqli pin=5.4.2 range-raw=yes rows-numeric=yes bound=no quoted=no CAKEPHP-WINDOW-SQLI-WITNESS
IOC do SQL compilado:
SELECT (COUNT(*) OVER (RANGE BETWEEN 1) /* CAKEPHP-WINDOW-SQLI-WITNESS */ -- PRECEDING AND CURRENT ROW)) AS n FROM sqlite_master
Controle:
SELECT (COUNT(*) OVER (ROWS BETWEEN 3 PRECEDING AND CURRENT ROW)) AS n FROM sqlite_master
Faça binding de offsets numéricos. Use allowlist para o tipo de frame (RANGE / ROWS / GROUPS) e a direção (PRECEDING / FOLLOWING). Para strings de intervalo, coloque-as entre aspas ou exija ExpressionInterface. Adicione um teste que garanta que um offset não-int de range() se torne um placeholder ou um literal entre aspas, e não SQL bruto.
887b641de9f7aba06071f98f17edcbc573cbcf63)WindowExpression.php · AggregateExpression.phpGNU Affero GPL v3.0. Veja LICENSE. Apenas lab em loopback. Sem garantia.