Files
zoneminder/web/ajax/control.php
T
Isaac ConnorandClaude Opus 5.5 c3d2e542bc fix: never build an object from an id the request sent as an array
ZM_Object's constructor treats an array as the object's row, and validInt()
and validCardinal() returned an array as they got it (preg_replace() does).
So wherever a request id reached `new ZM\X(...)`, raw or through those
validators, a crafted array built an object whose Id, UserId or MonitorId
the sender chose. The permission check then looked at those, and the delete
or update that followed used the Id. A user with Events=View deleted another
user's filter by posting Id[Id]=<filter>&Id[UserId]=<self> to the filter
delete action; the same shape reached events (MonitorId), users, reports,
snapshots and storage.

validInt() and validCardinal() now give '' for anything that isn't a single
value. The places that built an object from a raw $_REQUEST value (the
control and event ajax handlers, the event, filter, report, snapshot and
storage actions, view_hls, and the download and event detail modals) take
the id through validCardinal().

tests/php/test_request_ids_scalar.php covers the validators with arrays and
fails if web code builds an object straight from $_REQUEST, $_GET or $_POST.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 23:07:08 -04:00

77 lines
2.3 KiB
PHP

<?php
require_once('includes/control_functions.php');
require_once('includes/Monitor.php');
// Monitor control actions, require a monitor id and control view permissions for that monitor
if ( empty($_REQUEST['id']) )
ajaxError('No monitor id supplied');
if ( isset($_REQUEST['action']) and ($_REQUEST['action'] == 'monitorAction') ) {
// Fire one of this monitor's manually-triggered actions. Only the action id
// arrives from the browser; the command and its target are rebuilt from the
// stored row, so a request cannot name an arbitrary control method or drive
// a device the operator has no rights on.
require_once('includes/MonitorAction.php');
if ( !canView('Control', $_REQUEST['id']) )
ajaxError('Insufficient permissions');
if ( empty($_REQUEST['aid']) )
ajaxError('No action id supplied');
$monitorAction = ZM\MonitorAction::find_one(array(
'Id' => validInt($_REQUEST['aid']),
'MonitorId' => validInt($_REQUEST['id']),
'TriggerOn' => 'Manual',
'Enabled' => 1,
));
if ( !$monitorAction )
ajaxError('No such enabled manual action for this monitor');
$target = $monitorAction->TargetMonitor();
if ( !$target )
ajaxError('Action has no target monitor');
// Rights are needed on the device being driven, not just on the monitor the
// action hangs off.
if ( !canView('Control', $target->Id()) )
ajaxError('Insufficient permissions on the target monitor');
$ctrlCommand = $monitorAction->controlCommand();
if ( !$ctrlCommand )
ajaxError('Action has no usable command');
if ( $target->sendControlCommand($ctrlCommand) ) {
ajaxResponse('Success');
} else {
ajaxError('Failed');
}
return;
}
if ( canView('Control', $_REQUEST['id']) ) {
$monitor = new ZM\Monitor(validCardinal($_REQUEST['id']));
$ctrlCommand = buildControlCommand($monitor);
if ( !$ctrlCommand ) {
ajaxError('No command received');
return;
}
// Opt-in query: return the daemon's reply (e.g. light status) to the browser.
if ( !empty($_REQUEST['response']) ) {
ajaxResponse(array('status' => $monitor->sendControlCommandWithResponse($ctrlCommand)));
return;
}
if ( $monitor->sendControlCommand($ctrlCommand) ) {
ajaxResponse('Success');
} else {
ajaxError('Failed');
}
}
ajaxError('Unrecognised action or insufficient permissions');
?>