mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2026-08-03 00:37:21 -04:00
Fix RCE vulnerability via API config edit privilege escalation
Add RBAC checks to ConfigsController edit() and delete() requiring System=Edit permission, matching the pattern used by other controllers. Harden System/Readonly column checks with !empty() to handle missing columns gracefully. Fix command injection in Event.php by using ZM_PATH_FFMPEG constant with escapeshellarg() instead of hardcoded unsanitized ffmpeg call. Add is_executable() validation at all exec() sites using ZM_PATH_FFMPEG as defense-in-depth against poisoned config values. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -104,6 +104,13 @@ class ConfigsController extends AppController {
|
||||
* @return void
|
||||
*/
|
||||
public function edit($id = null) {
|
||||
global $user;
|
||||
$canEdit = (!$user) || ($user['System'] == 'Edit');
|
||||
if (!$canEdit) {
|
||||
throw new UnauthorizedException(__('Insufficient privileges'));
|
||||
return;
|
||||
}
|
||||
|
||||
$this->Config->id = $id;
|
||||
|
||||
if (!$this->Config->exists($id)) {
|
||||
@@ -112,9 +119,12 @@ class ConfigsController extends AppController {
|
||||
if ($this->request->is(array('post', 'put'))) {
|
||||
$options = array('conditions' => array('Config.' . $this->Config->primaryKey => $id));
|
||||
$config = $this->Config->find('first', $options);
|
||||
if ($config['Config']['System']) {
|
||||
throw new ForbiddenException(__('Cannot edit a system Config entry. Must be changed in /etc/zm/zm.conf'));
|
||||
}
|
||||
if (!empty($config['Config']['System'])) {
|
||||
throw new ForbiddenException(__('Cannot edit a system Config entry. Must be changed in /etc/zm/zm.conf'));
|
||||
}
|
||||
if (!empty($config['Config']['Readonly'])) {
|
||||
throw new ForbiddenException(__('Cannot edit a readonly Config entry'));
|
||||
}
|
||||
if ($this->Config->save($this->request->data)) {
|
||||
return $this->flash(__('The config has been saved.'), array('action' => 'index'));
|
||||
}
|
||||
@@ -132,6 +142,13 @@ class ConfigsController extends AppController {
|
||||
* @return void
|
||||
*/
|
||||
public function delete($id = null) {
|
||||
global $user;
|
||||
$canEdit = (!$user) || ($user['System'] == 'Edit');
|
||||
if (!$canEdit) {
|
||||
throw new UnauthorizedException(__('Insufficient privileges'));
|
||||
return;
|
||||
}
|
||||
|
||||
$this->Config->id = $id;
|
||||
if (!$this->Config->exists()) {
|
||||
throw new NotFoundException(__('Invalid config'));
|
||||
|
||||
@@ -454,8 +454,11 @@ class Event extends ZM_Object {
|
||||
return '';
|
||||
}
|
||||
|
||||
#$command ='ffmpeg -v 0 -i '.$videoPath.' -vf "select=gte(n\\,'.$frame['FrameId'].'),setpts=PTS-STARTPTS" '.$eventPath.'/'.$captureImage;
|
||||
$command ='ffmpeg -ss '. $frame['Delta'] .' -i '.$videoPath.' -frames:v 1 '.$eventPath.'/'.$captureImage;
|
||||
if ( !is_executable(ZM_PATH_FFMPEG) ) {
|
||||
Error('ZM_PATH_FFMPEG is not a valid executable: '.ZM_PATH_FFMPEG);
|
||||
return '';
|
||||
}
|
||||
$command = ZM_PATH_FFMPEG.' -ss '.escapeshellarg($frame['Delta']).' -i '.escapeshellarg($videoPath).' -frames:v 1 '.escapeshellarg($eventPath.'/'.$captureImage).' 2>&1';
|
||||
Debug('Running '.$command);
|
||||
$output = array();
|
||||
$retval = 0;
|
||||
|
||||
@@ -339,6 +339,11 @@ if ( empty($_REQUEST['path']) ) {
|
||||
}
|
||||
}
|
||||
if (file_exists($file_path)) {
|
||||
if ( !is_executable(ZM_PATH_FFMPEG) ) {
|
||||
header('HTTP/1.0 500 Internal Server Error');
|
||||
ZM\Error('ZM_PATH_FFMPEG is not a valid executable: '.ZM_PATH_FFMPEG);
|
||||
return;
|
||||
}
|
||||
$command = ZM_PATH_FFMPEG.' -ss '.escapeshellarg($Frame->Delta()).' -i '.escapeshellarg($file_path).' -frames:v 1 '.escapeshellarg($path).' 2>&1';
|
||||
#$command ='ffmpeg -ss '. $Frame->Delta() .' -i '.$Event->Path().'/'.$Event->DefaultVideo().' -vf "select=gte(n\\,'.$Frame->FrameId().'),setpts=PTS-STARTPTS" '.$path;
|
||||
#$command ='ffmpeg -v 0 -i '.$Storage->Path().'/'.$Event->Path().'/'.$Event->DefaultVideo().' -vf "select=gte(n\\,'.$Frame->FrameId().'),setpts=PTS-STARTPTS" '.$path;
|
||||
@@ -423,6 +428,11 @@ if ( empty($_REQUEST['path']) ) {
|
||||
ZM\Error("Can't create frame images from video because there is no video file for this event at (".$Event->Path().'/'.$Event->DefaultVideo() );
|
||||
return;
|
||||
}
|
||||
if ( !is_executable(ZM_PATH_FFMPEG) ) {
|
||||
header('HTTP/1.0 500 Internal Server Error');
|
||||
ZM\Error('ZM_PATH_FFMPEG is not a valid executable: '.ZM_PATH_FFMPEG);
|
||||
return;
|
||||
}
|
||||
// Use escapeshellarg() to prevent command injection
|
||||
$command = ZM_PATH_FFMPEG.' -ss '.escapeshellarg($Frame->Delta()).' -i '.escapeshellarg($file_path).' -frames:v 1 '.escapeshellarg($path).' 2>&1';
|
||||
#$command ='ffmpeg -ss '. $Frame->Delta() .' -i '.$Event->Path().'/'.$Event->DefaultVideo().' -vf "select=gte(n\\,'.$Frame->FrameId().'),setpts=PTS-STARTPTS" '.$path;
|
||||
|
||||
Reference in New Issue
Block a user