Files
Isaac ConnorandClaude Opus 4.8 49610fb685 fix: make StatesController::packageControl protected so it can't be called directly
packageControl() runs zmpkg.pl (which starts, stops and switches the run state
of ZoneMinder) with the state name from the request. It was public, so CakePHP
routed GET /api/states/packageControl/<command> straight to it, skipping the
System=Edit check in change() -- the method's only intended caller. With
beforeFilter() admitting any user whose System permission is not None, a
System=View user could stop, restart or change the run state.

Make it protected. change() still calls it after its System=Edit check; the
direct route now raises PrivateActionException (404).

Addresses GHSA-9gc6-9xg2-5636.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-23 22:29:38 -04:00

176 lines
4.1 KiB
PHP

<?php
App::uses('AppController', 'Controller');
/**
* States Controller
*
* @property State $State
* @property PaginatorComponent $Paginator
*/
class StatesController extends AppController {
public $components = array('RequestHandler');
public function beforeFilter() {
parent::beforeFilter();
global $user;
$canView = (!$user) || ($user->System() != 'None');
if ( !$canView ) {
throw new UnauthorizedException(__('Insufficient Privileges'));
return;
}
}
/**
* index method
*
* @return void
*/
public function index() {
$this->State->recursive = 0;
$states = $this->State->find('all');
$this->set(array(
'states' => $states,
'_serialize' => array('states')
));
}
/**
* view method
*
* @throws NotFoundException
* @param string $id
* @return void
*/
public function view($id = null) {
if ( !$this->State->exists($id) ) {
throw new NotFoundException(__('Invalid state'));
}
$options = array('conditions' => array('State.' . $this->State->primaryKey => $id));
$this->set(array(
'state' => $this->State->find('first', $options),
'_serialize' => array('state')
));
}
/**
* add method
*
* @return void
*/
public function add() {
if ($this->request->is('post')) {
global $user;
$canEdit = (!$user) || ($user->System() == 'Edit');
if ( !$canEdit ) {
throw new UnauthorizedException(__('Insufficient privileges'));
return;
}
$this->State->create();
if ($this->State->save($this->request->data)) {
$message = 'Saved';
} else {
$message = $this->State->validationErrors ? $this->State->validationErrors : 'Error';
}
$this->set(array(
'message' => $message,
'_serialize' => array('message')
));
}
}
/**
* edit method
*
* @throws NotFoundException
* @param string $id
* @return void
*/
public function edit($id = null) {
if (!$this->State->exists($id)) {
throw new NotFoundException(__('Invalid state'));
}
global $user;
$canEdit = (!$user) || ($user->System() == 'Edit');
if ( !$canEdit ) {
throw new UnauthorizedException(__('Insufficient privileges'));
return;
}
$this->request->allowMethod('post', 'put');
// Without the id, save() inserts a new state instead of updating this one.
$this->State->id = $id;
if ( $this->State->save($this->request->data) ) {
$message = 'Saved';
} else {
$message = $this->State->validationErrors ? $this->State->validationErrors : 'Error';
}
$this->set(array(
'message' => $message,
'_serialize' => array('message')
));
}
/**
* delete method
*
* @throws NotFoundException
* @param string $id
* @return void
*/
public function delete($id = null) {
$this->State->id = $id;
global $user;
$canEdit = (!$user) || ($user->System() == 'Edit');
if ( !$canEdit ) {
throw new UnauthorizedException(__('Insufficient privileges'));
return;
}
if (!$this->State->exists()) {
throw new NotFoundException(__('Invalid state'));
}
$this->request->allowMethod('post', 'delete');
if ($this->State->delete()) {
return $this->flash(__('The state has been deleted.'), array('action' => 'index'));
} else {
return $this->flash(__('The state could not be deleted. Please, try again.'), array('action' => 'index'));
}
}
public function change() {
global $user;
$canEdit = (!$user) || ($user->System() == 'Edit');
if ( !$canEdit ) {
throw new UnauthorizedException(__('Insufficient privileges'));
return;
}
$newState = $this->request->params['pass'][0];
$blah = $this->packageControl($newState);
$this->set(array(
'blah' => $blah,
'_serialize' => array('blah')
));
}
// protected, not public: a public controller method is directly routable
// (GET /api/states/packageControl/<cmd>), which would skip the System=Edit
// check in change() and let any user with System>=View run zmpkg.pl. change()
// still calls it after that check (refs GHSA-9gc6-9xg2-5636).
protected function packageControl( $command ) {
$zm_path_bin = Configure::read('ZM_PATH_BIN');
$string = $zm_path_bin.'/zmpkg.pl '.escapeshellarg( $command );
$status = exec( $string );
return $status;
}
}