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:
Isaac Connor
2026-02-26 13:39:31 -05:00
parent dfc7b54127
commit b036408a5b
3 changed files with 35 additions and 5 deletions

View File

@@ -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'));

View File

@@ -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;

View File

@@ -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;