mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2026-11-06 04:33:05 -05:00
Groups, Snapshots and Tags add/edit checked the ids from MonitorIds or
EventIds when the request had them, but Model::save() links the HABTM
entry (Monitor, Event) and ignores those fields on edit. A Groups=Edit
user could send {"Group":{"MonitorIds":"<current members>"},"Monitor":[]}:
the membership check saw no monitor moved, the save emptied the group and
lifted the Groups_Permissions deny on its monitors. On add, an empty
MonitorIds or EventIds next to a Monitor/Event list let monitors the user
may not edit into a new group, and events the user may not view into a
snapshot.
requestAssociatedIds() now reads the list the way save() does and
rewrites the request's entry to that plain list of ids, so a row given as
a join record links only its MonitorId/EventId. add() copies MonitorIds
or EventIds into the entry before the check, as it did before the save.
Test: tests/php/test_api_associated_ids.php
(cherry picked from commit 6bf015530e)
108 lines
5.0 KiB
PHP
108 lines
5.0 KiB
PHP
<?php
|
|
// Security regression test: Groups, Snapshots and Tags API writes must check the links the
|
|
// save will write.
|
|
//
|
|
// AppController::requestAssociatedIds() took the ids from $alias[MonitorIds|EventIds] when
|
|
// present, while Model::save() links what the request gives as the HABTM entry (Monitor,
|
|
// Event) and ignores those fields on edit. So {"Group":{"MonitorIds":"2,3"},"Monitor":[]}
|
|
// passed the membership check of group 2,3 as "nothing moved" and emptied the group, and
|
|
// {"Snapshot":{"EventIds":"1"},"Event":[2]} added an event the user may not view.
|
|
//
|
|
// AppController can't be loaded without CakePHP, so the methods are taken from the source and
|
|
// run in a stand-in with the same $this->request->data.
|
|
//
|
|
// Run as: php tests/php/test_api_associated_ids.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";
|
|
}
|
|
}
|
|
|
|
$controllers = __DIR__.'/../../web/api/app/Controller';
|
|
$source = file_get_contents($controllers.'/AppController.php');
|
|
$methods = '';
|
|
foreach (array('requestAssociatedIds') as $fn) {
|
|
if (!preg_match('/^ protected function '.$fn.'\(.*?^ }\n/ms', $source, $m)) {
|
|
echo "FAIL - could not find $fn in AppController.php\n";
|
|
exit(1);
|
|
}
|
|
$methods .= str_replace('protected function', 'public function', $m[0]);
|
|
}
|
|
|
|
// Stand-ins for what the methods call.
|
|
class UnauthorizedException extends Exception {}
|
|
function __($s) { return $s; }
|
|
$links = array();
|
|
function dbFetchAll($sql, $col, $args) {
|
|
global $links;
|
|
return isset($links[$args[0]]) ? $links[$args[0]] : array();
|
|
}
|
|
$viewable = array();
|
|
eval('namespace ZM; class Event {
|
|
private $id;
|
|
public function __construct($id) { $this->id = in_array($id, array(1, 2, 3)) ? $id : null; }
|
|
public function Id() { return $this->id; }
|
|
public function canView() { global $viewable; return in_array($this->id, $viewable); }
|
|
}');
|
|
|
|
eval('class AssocHarness { public $request; public $Group; public $Snapshot; public $checked = array();
|
|
public function requireEventsView($ids) {
|
|
global $viewable;
|
|
foreach ($ids as $id) if (!in_array($id, $viewable)) throw new UnauthorizedException("view");
|
|
}
|
|
'.$methods.' }');
|
|
|
|
function harness(array $data) {
|
|
$h = new AssocHarness();
|
|
$h->request = (object)array('data' => $data);
|
|
$h->Group = (object)array('hasAndBelongsToMany' => array('Monitor' => array('associationForeignKey' => 'MonitorId')));
|
|
$h->Snapshot = (object)array('hasAndBelongsToMany' => array('Event' => array('associationForeignKey' => 'EventId')));
|
|
return $h;
|
|
}
|
|
|
|
function ids(array $data, $alias = 'Group', $assoc = 'Monitor') {
|
|
$h = harness($data);
|
|
$ids = $h->requestAssociatedIds($alias, $assoc);
|
|
return array($ids, $h->request->data);
|
|
}
|
|
|
|
// What the check sees is what save() will link.
|
|
check('a plain list', ids(array('Monitor' => array('2', 3)))[0], array(2, 3));
|
|
check('a plain list is saved as given', ids(array('Monitor' => array('2', 3)))[1], array('Monitor' => array('Monitor' => array(2, 3))));
|
|
check('Monitor[Monitor] form', ids(array('Monitor' => array('Monitor' => array(4))))[0], array(4));
|
|
check('Monitor[Monitor] as a single value', ids(array('Monitor' => array('Monitor' => '5')))[0], array(5));
|
|
check('an empty list removes every link', ids(array('Monitor' => array()))[0], array());
|
|
check('MonitorIds next to Monitor: Monitor is what save() links',
|
|
ids(array('Group' => array('MonitorIds' => '2,3'), 'Monitor' => array()))[0], array());
|
|
check('MonitorIds alone links nothing on edit (save() ignores it)',
|
|
ids(array('Group' => array('MonitorIds' => '2,3')))[0], null);
|
|
check('null leaves the links alone', ids(array('Monitor' => null))[0], null);
|
|
check('a string leaves the links alone and is dropped',
|
|
ids(array('Group' => array('Name' => 'g'), 'Monitor' => 'x'))[1], array('Group' => array('Name' => 'g')));
|
|
check('a join record links only its MonitorId',
|
|
ids(array('Monitor' => array(array('Id' => 100000, 'MonitorId' => 4, 'GroupId' => 9))))[1],
|
|
array('Monitor' => array('Monitor' => array(4))));
|
|
check('a record without MonitorId links nothing', ids(array('Monitor' => array(array('Id' => 7))))[0], array());
|
|
check('events for a snapshot', ids(array('Snapshot' => array('EventIds' => '1'), 'Event' => array(2)), 'Snapshot', 'Event')[0], array(2));
|
|
|
|
// The controllers use them that way.
|
|
foreach (array('Groups' => array('Group', 'MonitorIds', 'Monitor'), 'Snapshots' => array('Snapshot', 'EventIds', 'Event'), 'Tags' => array('Tag', 'EventIds', 'Event')) as $name => $a) {
|
|
list($alias, $field, $assoc) = $a;
|
|
$c = file_get_contents("$controllers/{$name}Controller.php");
|
|
$copy = strpos($c, "\$this->request->data['$assoc'] = explode(',', \$this->request->data['$alias']['$field']);");
|
|
$first = strpos($c, "requestAssociatedIds('$alias', '$assoc')");
|
|
check("$name add copies $field to $assoc before the check", $copy !== false and $first !== false and $copy < $first, true);
|
|
}
|
|
|
|
echo "\n$passes passed, $failures failed\n";
|
|
exit($failures ? 1 : 0);
|