mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2026-10-14 16:51:08 -04:00
requestField() read Model[field] whenever the request's Model entry was an
array, empty or not. CakePHP's Model::set() treats the bare top-level fields
as the model's data when that entry is empty (_setAliasData), so
{"Monitor":[],"EventEndCommand":"x"} passed the System-edit check on the
monitor command fields, which found no value, while save() stored x. A
Monitors=Edit user without System=Edit could set EventStartCommand or
EventEndCommand, which zmc runs through /bin/sh.
Every other check built on requestField() had the same gap: the target
MonitorId when adding or editing an event, the EventId when adding a frame,
and the ParentId when editing a group.
Read Model[field] only when the Model entry is non-empty, as set() does, and
otherwise the bare field; a non-empty entry that is not an array yields
nothing, since set() skips it.
tests/php/test_api_request_field.php runs the real method against each
request shape.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
64 lines
3.1 KiB
PHP
64 lines
3.1 KiB
PHP
<?php
|
|
// Security regression test: AppController::requestField() must read a field from the request
|
|
// the way CakePHP's Model::set() will save it. refs GHSA-fp33-8fx5-4j6c
|
|
//
|
|
// Model::set() treats the bare top-level fields as the model's data when the request's
|
|
// Model entry is empty (empty($data[alias]) -> _setAliasData()). requestField() read
|
|
// Model[field] whenever Model was an array, so {"Monitor":[],"EventEndCommand":"x"} passed
|
|
// the System-edit check on the command fields (no value found) while save() stored x. The
|
|
// same shape slipped past every check built on requestField(): Event.MonitorId,
|
|
// Frame.EventId, Group.ParentId.
|
|
//
|
|
// AppController can't be loaded without CakePHP, so the method is taken from the source and
|
|
// run in a stand-in with the same $this->request->data.
|
|
//
|
|
// Run as: php tests/php/test_api_request_field.php
|
|
|
|
$failures = 0;
|
|
$passes = 0;
|
|
|
|
function check($name, $got, $want) {
|
|
global $failures, $passes;
|
|
if ($got === $want) {
|
|
$passes++;
|
|
echo "ok - $name\n";
|
|
} else {
|
|
$failures++;
|
|
echo "FAIL - $name (got ".var_export($got, true).", want ".var_export($want, true).")\n";
|
|
}
|
|
}
|
|
|
|
$source = file_get_contents(__DIR__.'/../../web/api/app/Controller/AppController.php');
|
|
if (!preg_match('/^ protected function requestField\(.*?^ }\n/ms', $source, $m)) {
|
|
echo "FAIL - could not find requestField in AppController.php\n";
|
|
exit(1);
|
|
}
|
|
eval('class RequestFieldHarness { public $request; '.str_replace('protected function', 'public function', $m[0]).' }');
|
|
|
|
function field(array $data, $alias, $name) {
|
|
$h = new RequestFieldHarness();
|
|
$h->request = (object)array('data' => $data);
|
|
return $h->requestField($alias, $name);
|
|
}
|
|
|
|
check('nested field is read',
|
|
field(array('Monitor' => array('EventEndCommand' => 'x')), 'Monitor', 'EventEndCommand'), 'x');
|
|
check('bare field is read when there is no Model entry',
|
|
field(array('EventEndCommand' => 'x'), 'Monitor', 'EventEndCommand'), 'x');
|
|
check('bare field is read when the Model entry is empty (Cake saves it)',
|
|
field(array('Monitor' => array(), 'EventEndCommand' => 'x'), 'Monitor', 'EventEndCommand'), 'x');
|
|
check('bare field is read when the Model entry is an empty string (Cake saves it)',
|
|
field(array('Monitor' => '', 'EventEndCommand' => 'x'), 'Monitor', 'EventEndCommand'), 'x');
|
|
check('bare field is ignored when the Model entry has data (Cake ignores it)',
|
|
field(array('Monitor' => array('Name' => 'a'), 'EventEndCommand' => 'x'), 'Monitor', 'EventEndCommand'), null);
|
|
check('nested field wins over a bare one',
|
|
field(array('Monitor' => array('EventEndCommand' => 'n'), 'EventEndCommand' => 'b'), 'Monitor', 'EventEndCommand'), 'n');
|
|
check('a non-array Model entry with data yields nothing (Cake skips it)',
|
|
field(array('Monitor' => 'junk', 'EventEndCommand' => 'x'), 'Monitor', 'EventEndCommand'), null);
|
|
check('Event MonitorId behind an empty Event entry is read',
|
|
field(array('Event' => array(), 'MonitorId' => '7'), 'Event', 'MonitorId'), '7');
|
|
check('missing field is null', field(array('Monitor' => array('Name' => 'a')), 'Monitor', 'EventEndCommand'), null);
|
|
|
|
echo "\n$passes passed, $failures failed\n";
|
|
exit($failures ? 1 : 0);
|