diff --git a/distros/beowulf/control b/distros/beowulf/control index fbb35a0f3..36cfd982f 100644 --- a/distros/beowulf/control +++ b/distros/beowulf/control @@ -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 diff --git a/distros/ubuntu2004/control b/distros/ubuntu2004/control index af002893d..4dcb3b486 100644 --- a/distros/ubuntu2004/control +++ b/distros/ubuntu2004/control @@ -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 diff --git a/web/includes/Object.php b/web/includes/Object.php index 3db86f641..b29d0c191 100644 --- a/web/includes/Object.php +++ b/web/includes/Object.php @@ -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 { diff --git a/web/includes/functions.php b/web/includes/functions.php index 4d5ad44a3..19b3cf11c 100644 --- a/web/includes/functions.php +++ b/web/includes/functions.php @@ -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 .= ''.PHP_EOL; } # end foreach options if ( $values and ((!is_array($values)) or count($values) ) and ! $has_selected ) { diff --git a/web/skins/classic/css/base/views/onvifprobe.css b/web/skins/classic/css/base/views/onvifprobe.css index 95e82cfdc..9a2def4ce 100644 --- a/web/skins/classic/css/base/views/onvifprobe.css +++ b/web/skins/classic/css/base/views/onvifprobe.css @@ -1,3 +1,7 @@ +.monitor-added { + opacity: 0.4; +} + .chosen-container { text-align: left; -} \ No newline at end of file +} diff --git a/web/skins/classic/views/js/watch.js b/web/skins/classic/views/js/watch.js index 5c10aece6..ff8e3e905 100644 --- a/web/skins/classic/views/js/watch.js +++ b/web/skins/classic/views/js/watch.js @@ -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(); diff --git a/web/skins/classic/views/onvifprobe.php b/web/skins/classic/views/onvifprobe.php index 3cd7201c5..6f62fd07e 100644 --- a/web/skins/classic/views/onvifprobe.php +++ b/web/skins/classic/views/onvifprobe.php @@ -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) diff --git a/web/views/image.php b/web/views/image.php index 4bb7619ad..a1b825cc3 100644 --- a/web/views/image.php +++ b/web/views/image.php @@ -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 diff --git a/web/views/view_hls.php b/web/views/view_hls.php index 59e89612d..3cdcb0ef2 100644 --- a/web/views/view_hls.php +++ b/web/views/view_hls.php @@ -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'; diff --git a/web/views/view_video.php b/web/views/view_video.php index ea825e2a3..f657e35a0 100644 --- a/web/views/view_video.php +++ b/web/views/view_video.php @@ -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 {