Merge branch 'master' into patch-180448

This commit is contained in:
IgorA100
2026-06-03 23:37:58 +03:00
committed by GitHub
10 changed files with 85 additions and 28 deletions

View File

@@ -13,7 +13,7 @@ Build-Depends: debhelper, sphinx-doc, dh-linktree, dh-apache2
,ffmpeg
,net-tools
,libbz2-dev
,libcurl4-gnutls-dev
,libcurl4-gnutls-dev | libcurl4-openssl-dev | libcurl4-nss-dev
,libturbojpeg0-dev
,default-libmysqlclient-dev | libmysqlclient-dev | libmariadbclient-dev-compat
,libpcre2-dev

View File

@@ -14,7 +14,7 @@ Build-Depends: debhelper (>= 11), sphinx-doc, python3-sphinx, python3-sphinx-rtd
,arp-scan
,net-tools, iproute2
,libbz2-dev
,libcurl4-gnutls-dev
,libcurl4-gnutls-dev | libcurl4-openssl-dev | libcurl4-nss-dev
,libjpeg-turbo8-dev | libjpeg62-turbo-dev | libjpeg8-dev | libjpeg9-dev
,libturbojpeg0-dev
,default-libmysqlclient-dev | libmysqlclient-dev | libmariadbclient-dev-compat
@@ -47,7 +47,6 @@ Depends: ${shlibs:Depends}, ${misc:Depends}, ${perl:Depends}
,libswscale9|libswscale8|libswscale7|libswscale6|libswscale5|libswscale4
,libswresample6|libswresample5|libswresample4|libswresample3|libswresample2
,ffmpeg
,libcurl4, libcurl4-gnutls-dev
,libdatetime-perl, libdate-manip-perl, libmime-lite-perl, libmime-tools-perl
,libdbd-mysql-perl
,libphp-serialization-perl

View File

@@ -24,7 +24,7 @@ class ZM_Object {
$table = $class::$table;
$row = dbFetchOne("SELECT * FROM `$table` WHERE `Id`=?", NULL, array($IdOrRow));
if (!$row) {
Error("Unable to load $class record for Id=$IdOrRow");
Warning("Unable to load $class record for Id=$IdOrRow");
return;
}
} else {

View File

@@ -424,7 +424,7 @@ function htmlOptions($options, $values) {
$has_selected = false;
foreach ( $options as $value=>$option ) {
$disabled = 0;
$text = '';
$text = $class = '';
if ( is_array($option) ) {
if ( isset($option['Name']) )
@@ -435,6 +435,9 @@ function htmlOptions($options, $values) {
if ( isset($option['disabled']) ) {
$disabled = $option['disabled'];
}
if ( isset($option['class']) ) {
$class = $option['class'];
}
} else if ( is_object($option) ) {
$text = $option->Name();
} else {
@@ -450,6 +453,7 @@ function htmlOptions($options, $values) {
$options_html .= '<option value="'.htmlspecialchars($value, ENT_COMPAT | ENT_HTML401, ini_get('default_charset'), false).'"'.
($selected?' selected="selected"':'').
($disabled?' disabled="disabled"':'').
($class?' class="'.htmlspecialchars($class, ENT_COMPAT | ENT_HTML401, ini_get('default_charset'), false).'"':'').
'>'.htmlspecialchars($text, ENT_COMPAT | ENT_HTML401, ini_get('default_charset'), false).'</option>'.PHP_EOL;
} # end foreach options
if ( $values and ((!is_array($values)) or count($values) ) and ! $has_selected ) {

View File

@@ -1,3 +1,7 @@
.monitor-added {
opacity: 0.4;
}
.chosen-container {
text-align: left;
}
}

View File

@@ -210,7 +210,7 @@ function streamCmdPlay(action) {
}
}
function streamCmdStop(action) {
function streamCmdStop() {
monitorStream.onplay = false; //Without this line, "onPlay" is triggered immediately due to "if (this.onplay) this.onplay();" in MonitorStream.js
//setButtonState('pauseBtn', 'inactive');
//setButtonState('playBtn', 'unavail');
@@ -221,9 +221,8 @@ function streamCmdStop(action) {
setButtonState('slowRevBtn', 'unavail');
setButtonState('fastRevBtn', 'unavail');
}
if (action) {
monitorStream.stop();
}
monitorStream.stop();
//setButtonState('stopBtn', 'unavail');
//setButtonState('playBtn', 'active');
setButtonStateWatch('playBtn', 'inactive');
@@ -1063,7 +1062,7 @@ function initPage() {
function stopPlayback() {
idleTimeoutTriggered = true;
streamCmdStop(true);
streamCmdStop();
const cycle_was = cycle;
cyclePause();
let ayswModal = $j('#AYSWModal');
@@ -1359,7 +1358,7 @@ function monitorChangeStreamChannel() {
monitorStream.currentChannelStream = streamChannel;
setCookie('zmStreamChannel', streamChannel);
if ((monitorStream.activePlayer) && (-1 !== monitorStream.activePlayer.indexOf('go2rtc') || -1 !== monitorStream.activePlayer.indexOf('rtsp2web'))) {
streamCmdStop(true);
streamCmdStop();
setTimeout(function() {
monitorStream.start(streamChannel);
onPlay();
@@ -1375,7 +1374,7 @@ function changePlayer() {
if (monitorStream.audioMotion && monitorStream.audioMotion.destroy) monitorStream.audioMotion.destroy();
monitorStream.destroyVolumeSlider();
streamCmdStop(true); // takes care of button state and calls stream.kill()
streamCmdStop(); // takes care of button state and calls stream.kill()
console.log('setting to ', $j('#player').val());
monitorStream.setPlayer($j('#player').val());
setChannelStream();

View File

@@ -169,15 +169,32 @@ if (!isset($_REQUEST['step']) || ($_REQUEST['step'] == '1')) {
}
}
$detcameras = probeCameras('');
foreach ($detcameras as $camera) {
if (preg_match('|([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)|', $camera['monitor']['Host'], $matches)) {
$ip = $matches[1];
$monitors = dbFetchAll('SELECT Path FROM Monitors WHERE Deleted=false');
$monitorHosts = [];
if ($monitors) {
foreach ($monitors as $monitor) {
$_host = parse_url($monitor['Path'], PHP_URL_HOST);
if ($_host) {
$monitorHosts[] = $_host;
}
}
$host = $ip;
}
$monitorHosts = array_unique($monitorHosts);
$detcameras = probeCameras('');
usort($detcameras, function($a, $b) {
return strcasecmp(parse_url($a['monitor']['Host'], PHP_URL_HOST) ?: '', parse_url($b['monitor']['Host'], PHP_URL_HOST) ?: '');
});
foreach ($detcameras as $camera) {
$host = parse_url($camera['monitor']['Host'], PHP_URL_HOST);
$sourceDesc = base64_encode(json_encode($camera['monitor']));
$sourceString = $camera['model'].' @ '.$host.' using version '.$camera['monitor']['SOAP'];
$cameras[$sourceDesc] = $sourceString;
$sourceString = htmlspecialchars($camera['model'].' @ '.$host.' using version '.$camera['monitor']['SOAP']);
if ($host && in_array($host, $monitorHosts, true)) {
$cameras[$sourceDesc] = ['Name'=> $sourceString, 'class'=> 'monitor-added'];
} else {
$cameras[$sourceDesc] = $sourceString;
}
}
if (count($cameras) <= 0)

View File

@@ -231,6 +231,13 @@ if ( empty($_REQUEST['path']) ) {
ZM\Error('Event '.$_REQUEST['eid'].' Not found');
return;
}
// Per-event ACL: coarse Events/Snapshots role isn't enough, must also check
// monitor-level permission (GHSA-vj5r-pc2v-gfwv). 404 to avoid leaking the id.
if (!$Event->canView()) {
header('HTTP/1.0 404 Not Found');
ZM\Warning('Event '.$_REQUEST['eid'].' access denied');
return;
}
if ( $_REQUEST['fid'] == 'objdetect' ) {
// if animation file is found, return that, else return image
@@ -418,6 +425,13 @@ if ( empty($_REQUEST['path']) ) {
ZM\Error('Event ' . $Frame->EventId() . ' Not Found');
return;
}
// Per-event ACL: see GHSA-vj5r-pc2v-gfwv. The frame id is user-supplied so the
// event/monitor it resolves to may be one the user is denied from viewing.
if (!$Event->canView()) {
header('HTTP/1.0 404 Not Found');
ZM\Warning('Event '.$Frame->EventId().' access denied via frame '.$_REQUEST['fid']);
return;
}
$path = $Event->Path().'/'.sprintf('%0'.ZM_EVENT_IMAGE_DIGITS.'d',$Frame->FrameId()).'-'.$show.'.jpg';
} # end if have eid

View File

@@ -22,6 +22,14 @@ if (!$Event->Id()) {
header('HTTP/1.1 404 Not Found');
die('Event not found');
}
// Per-event ACL: coarse canView('Events') isn't enough — the user may be denied
// access to the monitor that owns this event (GHSA-vj5r-pc2v-gfwv). Return the
// same 404 as a missing event so the id isn't leaked.
if (!$Event->canView()) {
ZM\Warning('Event '.$_REQUEST['eid'].' HLS access denied');
header('HTTP/1.1 404 Not Found');
die('Event not found');
}
$m3u8_path = $Event->Path() . '/index.m3u8';

View File

@@ -40,15 +40,27 @@ $mode = (!empty($_REQUEST['mode'])) ? $_REQUEST['mode'] : '';
$Event = null;
if ( ! empty($_REQUEST['eid']) ) {
$Event = new ZM\Event($_REQUEST['eid']);
if (!empty($_REQUEST['file'])) {
$path = $Event->Path().'/'.basename($_REQUEST['file']);
} else {
$path = $Event->Path().'/'.$Event->DefaultVideo();
$event_id = !empty($_REQUEST['eid']) ? $_REQUEST['eid']
: (!empty($_REQUEST['event_id']) ? $_REQUEST['event_id'] : null);
if ($event_id !== null) {
$Event = new ZM\Event($event_id);
// Validate the event actually loaded — the constructor silently produces an
// empty object for unknown ids. Without this check view_video previously
// returned HTTP 200 with an empty body for nonexistent ids.
if (!$Event->Id()) {
header('HTTP/1.0 404 Not Found');
ZM\Error('Event '.$event_id.' Not found');
die();
}
// Per-event ACL: coarse canView('Events') isn't enough — the user may be
// denied access to the monitor that owns this event (GHSA-vj5r-pc2v-gfwv).
// 404 matches the missing-event response so the id isn't leaked.
if (!$Event->canView()) {
header('HTTP/1.0 404 Not Found');
ZM\Warning('Event '.$event_id.' access denied');
die();
}
} else if ( ! empty($_REQUEST['event_id']) ) {
$Event = new ZM\Event($_REQUEST['event_id']);
if (!empty($_REQUEST['file'])) {
$path = $Event->Path().'/'.basename($_REQUEST['file']);
} else {