From 54e180fad0c3273c798bd6b2460287da32193409 Mon Sep 17 00:00:00 2001 From: IgorA100 Date: Thu, 5 Mar 2026 18:28:58 +0300 Subject: [PATCH 1/9] Added the ManageEventListener class, which allows for more flexible event listener assignment and removal. (skin.js) This is especially important when using the "bind()" method, as this method creates a new function, and removing the listener is only possible with a reference function. --- web/skins/classic/js/skin.js | 40 ++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/web/skins/classic/js/skin.js b/web/skins/classic/js/skin.js index c5eb6f383..899e3717f 100644 --- a/web/skins/classic/js/skin.js +++ b/web/skins/classic/js/skin.js @@ -2862,4 +2862,44 @@ const waitUntil = (condition, timeout = 0) => { }); }; +// https://stackoverflow.com/a/69273090 +class ManageEventListener { + #listeners = {}; // # in a JS class signifies private + #idx = 1; + + // add event listener, returns integer ID of new listener + addEventListener(element, type, listener, useCapture = false) { + this.#privateAddEventListener(element, this.#idx, type, listener, useCapture); + return this.#idx++; + } + + // add event listener with custom ID (avoids need to retrieve return ID since you are providing it yourself) + addEventListenerById(element, id, type, listener, useCapture = false) { + this.#privateAddEventListener(element, id, type, listener, useCapture); + return id; + } + + #privateAddEventListener(element, id, type, listener, useCapture) { + if (this.#listeners[id]) throw Error(`A listener with id ${id} already exists`); + element.addEventListener(type, listener, useCapture); + this.#listeners[id] = {element, type, listener, useCapture}; + } + + // remove event listener with given ID, returns ID of removed listener or null (if listener with given ID does not exist) + removeEventListener(id) { + const listen = this.#listeners[id]; + if (listen) { + listen.element.removeEventListener(listen.type, listen.listener, listen.useCapture); + delete this.#listeners[id]; + } + return !!listen ? id : null; + } + + // returns number of events listeners + length() { + return Object.keys(this.#listeners).length; + } +} +const manageEventListener = new ManageEventListener(); + $j( window ).on("load", initPageGeneral); From 2f9839b20b5cd2187522d41684a99cc19751c6c1 Mon Sep 17 00:00:00 2001 From: IgorA100 Date: Thu, 5 Mar 2026 19:01:25 +0300 Subject: [PATCH 2/9] To assign the 'beforeunload' listener and remove the listener, we now use the ManageEventListener class (MonitorStream.js) --- web/js/MonitorStream.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/web/js/MonitorStream.js b/web/js/MonitorStream.js index 91259a701..490e00d7e 100644 --- a/web/js/MonitorStream.js +++ b/web/js/MonitorStream.js @@ -1,6 +1,7 @@ "use strict"; var janus = null; const streaming = []; +const handlerEventListener = []; function MonitorStream(monitorData) { this.id = monitorData.id; @@ -441,7 +442,7 @@ function MonitorStream(monitorData) { clearInterval(this.statusCmdTimer); // Fix for issues in Chromium when quickly hiding/showing a page. Doesn't clear statusCmdTimer when minimizing a page https://stackoverflow.com/questions/9501813/clearinterval-not-working this.statusCmdTimer = setInterval(this.statusCmdQuery.bind(this), statusRefreshTimeout); this.started = true; - this.streamListenerBind(); + handlerEventListener['killStream'] = this.streamListenerBind(); if (typeof observerMontage !== 'undefined') observerMontage.observe(stream); this.activePlayer = 'go2rtc'; @@ -482,7 +483,7 @@ function MonitorStream(monitorData) { attachVideo(this); this.statusCmdTimer = setInterval(this.statusCmdQuery.bind(this), statusRefreshTimeout); this.started = true; - this.streamListenerBind(); + handlerEventListener['killStream'] = this.streamListenerBind(); this.activePlayer = 'janus'; this.updateStreamInfo('Janus', 'loading'); return; @@ -554,7 +555,7 @@ function MonitorStream(monitorData) { clearInterval(this.statusCmdTimer); // Fix for issues in Chromium when quickly hiding/showing a page. Doesn't clear statusCmdTimer when minimizing a page https://stackoverflow.com/questions/9501813/clearinterval-not-working this.statusCmdTimer = setInterval(this.statusCmdQuery.bind(this), statusRefreshTimeout); this.started = true; - this.streamListenerBind(); + handlerEventListener['killStream'] = this.streamListenerBind(); this.updateStreamInfo((typeof players !== "undefined" && players) ? players[this.activePlayer] : 'RTSP2Web ' + this.RTSP2WebType, 'loading'); return; } else { @@ -618,12 +619,14 @@ function MonitorStream(monitorData) { } } // end if paused or not this.started = true; - this.streamListenerBind(); + handlerEventListener['killStream'] = this.streamListenerBind(); this.activePlayer = 'zms'; this.updateStreamInfo('ZMS MJPEG'); }; // this.start this.stop = function() { + manageEventListener.removeEventListener(handlerEventListener['killStream']); + /* Stop should stop the stream (killing zms) but NOT set src=''; This leaves the last jpeg up on screen instead of a broken image */ const stream = this.getElement(); if (!stream) { @@ -1976,10 +1979,10 @@ function startRTSP2WebPlay(videoEl, url, stream) { } function streamListener(stream) { - window.addEventListener('beforeunload', function(event) { + return manageEventListener.addEventListener(window, 'beforeunload', function() { console.log('streamListener'); stream.kill(); - }); + }, false); } function mseListenerSourceopen(context, videoEl, url) { From 840db3b993149c8f919deb0514a45cd80a95116d Mon Sep 17 00:00:00 2001 From: IgorA100 Date: Thu, 5 Mar 2026 19:05:48 +0300 Subject: [PATCH 3/9] Removed extra space (skin.js) --- web/skins/classic/js/skin.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/skins/classic/js/skin.js b/web/skins/classic/js/skin.js index 899e3717f..74284a706 100644 --- a/web/skins/classic/js/skin.js +++ b/web/skins/classic/js/skin.js @@ -2864,7 +2864,7 @@ const waitUntil = (condition, timeout = 0) => { // https://stackoverflow.com/a/69273090 class ManageEventListener { - #listeners = {}; // # in a JS class signifies private + #listeners = {}; // # in a JS class signifies private #idx = 1; // add event listener, returns integer ID of new listener @@ -2872,7 +2872,7 @@ class ManageEventListener { this.#privateAddEventListener(element, this.#idx, type, listener, useCapture); return this.#idx++; } - + // add event listener with custom ID (avoids need to retrieve return ID since you are providing it yourself) addEventListenerById(element, id, type, listener, useCapture = false) { this.#privateAddEventListener(element, id, type, listener, useCapture); From 15cb090d9591c0a3c42e318cfb98518438438ea6 Mon Sep 17 00:00:00 2001 From: IgorA100 Date: Fri, 6 Mar 2026 10:46:09 +0300 Subject: [PATCH 4/9] Instead of handlerEventListener, we use this.handlerEventListener (MonitorStream.js). We won't use the "this.killStreamListenerId" variable recommended by Copilot, but will instead use the this.handlerEventListener array, as we may have other listeners in the future. Using an array will eliminate the need to declare additional constants. --- web/js/MonitorStream.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/web/js/MonitorStream.js b/web/js/MonitorStream.js index 490e00d7e..2a2562b28 100644 --- a/web/js/MonitorStream.js +++ b/web/js/MonitorStream.js @@ -1,7 +1,6 @@ "use strict"; var janus = null; const streaming = []; -const handlerEventListener = []; function MonitorStream(monitorData) { this.id = monitorData.id; @@ -31,6 +30,7 @@ function MonitorStream(monitorData) { this.wsMSE = null; this.streamStartTime = 0; // Initial point of flow start time. Used for flow lag time analysis. this.waitingStart; + this.handlerEventListener = []; this.mseListenerSourceopenBind = null; this.streamListenerBind = null; this.mseSourceBufferListenerUpdateendBind = null; @@ -442,7 +442,7 @@ function MonitorStream(monitorData) { clearInterval(this.statusCmdTimer); // Fix for issues in Chromium when quickly hiding/showing a page. Doesn't clear statusCmdTimer when minimizing a page https://stackoverflow.com/questions/9501813/clearinterval-not-working this.statusCmdTimer = setInterval(this.statusCmdQuery.bind(this), statusRefreshTimeout); this.started = true; - handlerEventListener['killStream'] = this.streamListenerBind(); + this.handlerEventListener['killStream'] = this.streamListenerBind(); if (typeof observerMontage !== 'undefined') observerMontage.observe(stream); this.activePlayer = 'go2rtc'; @@ -483,7 +483,7 @@ function MonitorStream(monitorData) { attachVideo(this); this.statusCmdTimer = setInterval(this.statusCmdQuery.bind(this), statusRefreshTimeout); this.started = true; - handlerEventListener['killStream'] = this.streamListenerBind(); + this.handlerEventListener['killStream'] = this.streamListenerBind(); this.activePlayer = 'janus'; this.updateStreamInfo('Janus', 'loading'); return; @@ -555,7 +555,7 @@ function MonitorStream(monitorData) { clearInterval(this.statusCmdTimer); // Fix for issues in Chromium when quickly hiding/showing a page. Doesn't clear statusCmdTimer when minimizing a page https://stackoverflow.com/questions/9501813/clearinterval-not-working this.statusCmdTimer = setInterval(this.statusCmdQuery.bind(this), statusRefreshTimeout); this.started = true; - handlerEventListener['killStream'] = this.streamListenerBind(); + this.handlerEventListener['killStream'] = this.streamListenerBind(); this.updateStreamInfo((typeof players !== "undefined" && players) ? players[this.activePlayer] : 'RTSP2Web ' + this.RTSP2WebType, 'loading'); return; } else { @@ -619,13 +619,13 @@ function MonitorStream(monitorData) { } } // end if paused or not this.started = true; - handlerEventListener['killStream'] = this.streamListenerBind(); + this.handlerEventListener['killStream'] = this.streamListenerBind(); this.activePlayer = 'zms'; this.updateStreamInfo('ZMS MJPEG'); }; // this.start this.stop = function() { - manageEventListener.removeEventListener(handlerEventListener['killStream']); + manageEventListener.removeEventListener(this.handlerEventListener['killStream']); /* Stop should stop the stream (killing zms) but NOT set src=''; This leaves the last jpeg up on screen instead of a broken image */ const stream = this.getElement(); From dbd52087d3bd64a5f1a49ea04cad0fface125923 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Sat, 7 Mar 2026 17:35:02 -0500 Subject: [PATCH 5/9] Update web/js/MonitorStream.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- web/js/MonitorStream.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/js/MonitorStream.js b/web/js/MonitorStream.js index 2a2562b28..f6126ea72 100644 --- a/web/js/MonitorStream.js +++ b/web/js/MonitorStream.js @@ -30,7 +30,7 @@ function MonitorStream(monitorData) { this.wsMSE = null; this.streamStartTime = 0; // Initial point of flow start time. Used for flow lag time analysis. this.waitingStart; - this.handlerEventListener = []; + this.handlerEventListener = {}; this.mseListenerSourceopenBind = null; this.streamListenerBind = null; this.mseSourceBufferListenerUpdateendBind = null; From 7532cfbabfac03093c87ddb59cbca4989eac10f5 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Sat, 7 Mar 2026 22:36:01 -0500 Subject: [PATCH 6/9] Try out recommended options for debuild when doing binary --- utils/do_debian_package.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/do_debian_package.sh b/utils/do_debian_package.sh index b1cd6922d..207f244f9 100755 --- a/utils/do_debian_package.sh +++ b/utils/do_debian_package.sh @@ -315,7 +315,7 @@ EOF sudo apt-get install devscripts equivs sudo mk-build-deps -ir $DIRECTORY.orig/debian/control echo "Status: $?" - DEBUILD=debuild + DEBUILD=debuild -b -uc -us else if [ $TYPE == "local" ]; then # Auto-install all ZoneMinder's dependencies using the Debian control file From 605f962b7b6ba7f5780b3e6715b5418b8b2addca Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Sat, 7 Mar 2026 22:46:38 -0500 Subject: [PATCH 7/9] fix: sanitize exportFile and connkey params in event export to prevent command injection The exportFile POST parameter was passed unsanitized to exportEvents() as $export_root, which was then interpolated directly into shell commands executed via exec(). An authenticated user with View permissions for Events could inject arbitrary shell commands through the exportFile parameter. The connkey parameter had the same vulnerability. Strip both values to safe characters (word chars, hyphens, dots) on entry to exportEvents(), and wrap the directory argument to tar/zip with escapeshellarg() as defense in depth. Co-Authored-By: Claude Opus 4.6 --- web/skins/classic/includes/export_functions.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/web/skins/classic/includes/export_functions.php b/web/skins/classic/includes/export_functions.php index aa6875e87..75a26deb4 100644 --- a/web/skins/classic/includes/export_functions.php +++ b/web/skins/classic/includes/export_functions.php @@ -898,6 +898,11 @@ function exportEvents( return false; } + // Sanitize user-supplied values used in file paths and shell commands + $export_root = preg_replace('/[^\w\-.]/', '', $export_root); + if (empty($export_root)) $export_root = 'zmExport'; + $connkey = preg_replace('/[^\w\-.]/', '', $connkey); + if (!($exportFormat == 'tar' or $exportFormat == 'zip')) { ZM\Error("None or invalid exportFormat specified $exportFormat."); return false; @@ -1027,7 +1032,7 @@ function exportEvents( } // if $exportFormat @unlink($archive_path); - $command .= ' '.$export_root.($connkey?'_'.$connkey:'').'/'; + $command .= ' '.escapeshellarg($export_root.($connkey?'_'.$connkey:'').'/'); ZM\Debug($command); exec($command, $output, $status); if ($status) { From e77993e35f013fbd9dc59f5828266064554c1be4 Mon Sep 17 00:00:00 2001 From: IgorA100 Date: Sun, 8 Mar 2026 12:38:45 +0300 Subject: [PATCH 8/9] Instead of "useCapture" we use the extended version "options" MonitorStream.js --- web/js/MonitorStream.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/js/MonitorStream.js b/web/js/MonitorStream.js index f6126ea72..e2f9af2c1 100644 --- a/web/js/MonitorStream.js +++ b/web/js/MonitorStream.js @@ -1982,7 +1982,7 @@ function streamListener(stream) { return manageEventListener.addEventListener(window, 'beforeunload', function() { console.log('streamListener'); stream.kill(); - }, false); + }, {capture: false}); } function mseListenerSourceopen(context, videoEl, url) { From 8ecaff07517a6ee9a12000638e7a3a17211e3e3a Mon Sep 17 00:00:00 2001 From: IgorA100 Date: Sun, 8 Mar 2026 12:44:12 +0300 Subject: [PATCH 9/9] Instead of "useCapture" we use the extended version "options" (skin.js) We also explicitly attach the manageEventListener to the window. --- web/skins/classic/js/skin.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/web/skins/classic/js/skin.js b/web/skins/classic/js/skin.js index 74284a706..3a632b3dc 100644 --- a/web/skins/classic/js/skin.js +++ b/web/skins/classic/js/skin.js @@ -2868,28 +2868,28 @@ class ManageEventListener { #idx = 1; // add event listener, returns integer ID of new listener - addEventListener(element, type, listener, useCapture = false) { - this.#privateAddEventListener(element, this.#idx, type, listener, useCapture); + addEventListener(element, type, listener, options = {}) { + this.#privateAddEventListener(element, this.#idx, type, listener, options); return this.#idx++; } // add event listener with custom ID (avoids need to retrieve return ID since you are providing it yourself) - addEventListenerById(element, id, type, listener, useCapture = false) { - this.#privateAddEventListener(element, id, type, listener, useCapture); + addEventListenerById(element, id, type, listener, options = {}) { + this.#privateAddEventListener(element, id, type, listener, options); return id; } - #privateAddEventListener(element, id, type, listener, useCapture) { + #privateAddEventListener(element, id, type, listener, options) { if (this.#listeners[id]) throw Error(`A listener with id ${id} already exists`); - element.addEventListener(type, listener, useCapture); - this.#listeners[id] = {element, type, listener, useCapture}; + element.addEventListener(type, listener, options); + this.#listeners[id] = {element, type, listener, options}; } // remove event listener with given ID, returns ID of removed listener or null (if listener with given ID does not exist) removeEventListener(id) { const listen = this.#listeners[id]; if (listen) { - listen.element.removeEventListener(listen.type, listen.listener, listen.useCapture); + listen.element.removeEventListener(listen.type, listen.listener, listen.options); delete this.#listeners[id]; } return !!listen ? id : null; @@ -2901,5 +2901,6 @@ class ManageEventListener { } } const manageEventListener = new ManageEventListener(); +window.manageEventListener = manageEventListener; $j( window ).on("load", initPageGeneral);