ZoneMinder 1.38.2 Release Notes
This is a maintenance release that includes security fixes, crash and memory-safety fixes, and a wide range of bug fixes for ZoneMinder 1.38.
Key Highlights
🔒 Security & Hardening
- API privilege escalation (RCE) - Added System=Edit RBAC checks to ConfigsController edit/delete and hardened ffmpeg path handling in Event.php
- GHSA-g66m-77fq-79v9 - Sanitized monitor Device path to prevent command injection
- GHSA-745h-vg7c-73cg - Escaped URLs in camera probe wget() to prevent command injection
- eval()-based RCE in filters - Replaced eval() in FilterTerm SystemLoad/DiskPercent/DiskBlocks evaluation with a safe compare() method and operator allowlist
- Command injection - Hardened onvifprobe.php, zmvideo.pl invocation, MonitorsController zmu calls, HostController du, event export, and download/export pipelines with escapeshellarg() and input sanitization
- SQL injection - Validated FilterTerm operators and collate, intval'd AlarmedZoneId/monitor IDs, restricted getFormChanges column keys, dbEscape'd MIME fields
- SSRF - Restricted image.php proxy URL scheme to http/https only
- XSS sanitization series - Removed reflected user input from add_monitors, device, event, events, log, and filterdebug AJAX sinks; sanitized export filename/connkey at the AJAX boundary; suppressed raw SQL error text in events ajax responses
- Auth hash bypass with reverse proxy - HTTP_X_FORWARDED_FOR is now used consistently in both PHP and C++ auth-hash validation when AUTH_HASH_IPS is enabled
- Reverse-proxy username handling - Auth relay now carries the username so zms validates against the indexed Username column instead of brute-iterating users
- Bumped
firebase/php-jwtfrom 6.0.0 to 7.0.0
🐛 Crash & Memory Safety
- VideoStore use-after-free - filename is now held by value; destructor guards against null oc when open() bailed early (refs #4757)
- MonitorStream segfault - Added monitor mutex to block shared-memory access during loadMonitor() remap; bail early when monitor failed to load
- MPEG stream codec failure - Prevented segfault when the codec fails to open
- MonitorLink fd leak - connect() now calls disconnect() first, avoiding "Too many open files" under repeated reconnect attempts
- VideoStore fd/codec leaks - Free video_out_ctx after failed avcodec_open2; clean up codec/hw_device contexts on setup_hwaccel failure; skip flush_codecs when no frames were sent
- Daemon FD reuse memory corruption - Daemonization now redirects stdin/stdout/stderr to /dev/null instead of closing them, avoiding libx264 writing into a reused stderr FD
- Null pointer guards - StorageId in Monitor::Load, ModelId in Monitor::Model(), evtStream element, monitor div in video-stream.js
- Empty events in ONDEMAND mode - Removed write-index guard that caused rapid Pause/Play cycling and ~2 empty events per second
🎯 ONVIF Improvements
- Subscription cleanup before re-Subscribe - Stops the NotAuthorized loop caused by leaked pull-point slots on Hikvision/Reolink cameras
- OOB read on negative gsoap codes - Replaced SOAP_STRINGS[] array with a switch covering the codes seen in practice (SOAP_EOF, network/SSL block, SOAP_STOP) (fixes #4842)
- Credential fallback chain - Falls back to Monitor ONVIF_Username/Password then Monitor User/Pass when ControlAddress lacks auth
- 'Use ONVIF' badge - Only displayed on the console when the listener is actually enabled
🎥 Recording & Playback Correctness
- AV_NOPTS_VALUE handling in VideoStore - Synthesizes monotonic DTS for video passthrough; reorder queue and audio first-DTS no longer compare AV_NOPTS_VALUE sentinels
- DTS backward jump detection - FfmpegCamera now triggers reconnect on >10s DTS rollback, ending the minutes-long warning storm after stream restarts
- Event end time - Derived from StartDateTime + Length when EndDateTime is missing (crashed/killed zmc), so montage review no longer paints a bar across hours of down-time
- incomplete.mp4 playback - C++ handler, view_video.php, and image.php now load incomplete event files; mp4 export end-timestamp fixed for multi-event downloads (closes #4767, #4774)
- HTTP Range header parsing - Correctly handles
bytes=start-end,bytes=start-, andbytes=-suffix; fixes ERR_CONTENT_LENGTH_MISMATCH on HEVC mp4 playback in Chrome (fixes #4777) - Content-Range header - Added missing dash separator
- Event::delete deadlock retry - Wraps the delete transaction in READ COMMITTED with rollback + retry on errno 1213 instead of committing through the deadlock
- Polygon fill - Scan-line fill now steps active edges by pairs, fixing non-convex zones whose concave gaps were incorrectly marked inside the zone
- Polygon clamp - Percentage polygons clamp to width-1/height-1, stopping spurious "polygon hi_x >= image width" warnings
- Zone stats percentage - Convert Area from percentage coordinate space (0-10000) to pixel area before calculating percentages
- alarm_frame_count in ready_count - Stops "Hit end of packetqueue before satisfying pre_event_count" warnings when pre_event_count is 0 but alarm_frame_count is set
🔐 Authentication & Permissions
- Role-based stream access - The C++ User class is now role-aware and consults Role_Monitors_Permissions, Role_Groups_Permissions, and User_Roles base permissions, matching the PHP visibleMonitor() logic
- Stale auth hash on long-running montage - MonitorStream.js refreshes the auth hash before restarting streams on tab visibility change and after the idle modal
- Auth relay - Username included in relay; empty auth_relay no longer produces double
&&in zms URLs; warns on user mismatch instead of silently falling back - Trigger reliability - zmTriggerEventOn now writes trigger_state last, fixing a ~1/3 trigger acceptance rate
🏗️ Build & PHP 8
- a2enmod rewrite - Debian/Ubuntu postinst now enables mod_rewrite (was incorrectly calling
a2enmod cgi), fixing install failures - PHP 7 polyfill - str_starts_with/str_ends_with/str_contains polyfilled at the top of functions.php so PHP 7 installs don't fatal on event.php
- PHP 8 GD handling - imagedestroy() now called conditionally; GDImage memory release forced on PHP >= 8.0
- www-data video group - Debian/Ubuntu postinst adds www-data to video and dialout groups so zmc can open /dev/video* on fresh installs (refs #4642)
- FreeBSD support - Top command parsing fixed for FreeBSD 13.5; /proc/meminfo presence check; removed unnecessary kFreeBSD arch checks; FreeBSD arm build fixes
- ZM_NO_CURL=ON build - Added HAVE_LIBCURL preprocessor guards throughout zm_monitor.h/cpp and zmc.cpp
Detailed Changes
Core Engine
- Default ffmpeg decoder thread_count to 2 (roughly halves software 1080p H.264 decode time)
- Flush decoder_queue on decoder thread exit to avoid stale latency offset across reconnect
- Reset last_write_index in Pause() to restore DECODING_ONDEMAND bootstrap
- Added CLOSE_DURATION event close mode handling (was silently falling back to CLOSE_IDLE)
- Event-id latch on linked monitor score detection so brief alarm cycles between analysis ticks are no longer missed
- Reduced linked monitor reconnect throttle from 60s to 1s
- Used
+1instead of+last_durationfor equal-DTS fixup
Camera Support
- FFmpeg: Use CxxUrl for credential injection (replaces fragile substr-based string manipulation)
- Reolink: Handle HTTP-to-HTTPS 302 redirect during login (LWP::UserAgent does not follow redirects on POST)
- V4L2: Treat ENOTTY like EINVAL when querying JPEG compression options (some drivers return ENOTTY)
- Added SSL certificate verification fallback to the base Control class — retries with verification disabled on SSL errors, applies to get/put/post
Web Interface & API
- pushState URL state management on the events page so filter state is shareable and browser back/forward restores it
- Mobile layout fixes for monitor config and watch views (CSS specificity on
:first-child) - Don't blank the screen between events when animations are off
- Alarm/idle border applied only to
.imageFeed, not nested img/video elements — fixes extra borders and streams jumping around - Stream URL state: include port in Server URL methods for port-forwarded setups (fixes #4675); honor explicit port argument in urlToApi single-server case; urlToApi falls back to location.host
- Thumbnail overlay scale calculated from actual monitor dimensions instead of hardcoded scale=75
- Montage review playback smoothness, fractional-seconds preserved through mmove/setSpeed, video seek overshoot fix on initial AVSEEK_FLAG_FRAME
- montagereview cookie stores speed value instead of index; changeFilters guards against NaN from invalid date input
- getTracksFromStream moved to skin.js so it can also be used on recorded events; added Go2RTC variant; restart MSE thread on any appendMseBuffer error (not just QuotaExceededError)
- VolumeSlider: noUiSlider properly destroyed when switching player or monitor;
#volumeControlsnow always includes the monitor ID - Event page: VID vs MJPEG playClicked/pauseClicked separation; toggleZones listener assigned after Event page loads
- MSE addSourceBuffer guarded against detached MediaSource when WebRTC wins the race in video-rtc.js
- RTSP2Web RTC errors restart the stream instead of killing it
- z-index ordering for zones SVG overlay corrected (cannot exceed .zonePoint index)
- Sort events by Tags column alias rather than T.Name (which is out of scope in the aggregated query)
- navbar_type now saved in cookies
- Move tag-related commands to canView(Events) instead of canEdit(Events); add Create to canEdit
- Filter debug modal: strip SKIP LOCKED from EXPLAIN so MySQL accepts the query
- Don't add postLoginQuery to the URL when empty
- Fixed image.php
.$fileconcatenation error
Database & Filters
- Filter::Sql() now clears accumulated state (PostSQLConditions, HasDiskPercent, HasDiskBlocks, HasSystemLoad) before rebuilding — previously grew unboundedly
- Filter::Execute uses
prepareinstead ofprepare_cachedsince the SQL changes every cycle (the cache never hit; entries leaked one per distinct substituted value) - zmfilter uses the minimum per-filter delay instead of the last filter's delay, so no filter oversleeps its ExecuteInterval; overdue warning now uses the unclamped delay and includes the filter name
- Handle PostSQLConditions being an empty array; don't Fatal on SQL prepare errors
- zmDbDo error logging substitutes all SQL placeholders (was only substituting the first)
- Log bind params correctly when SQL contains literal
%characters
Configuration & Logging
- Increased potential config line size — HTML snippets can easily exceed 256 bytes
- Enriched zms auth-failure warning with diagnostic fields to distinguish stale hash, missing auth, disabled user, and IP mismatch
- Removed Warn() in favour of Warning() (fixes #4724)
- Log commands used when updating the database
Scripts & Tools
- zmcontrol/Control.pm: Base class SSL fallback applied across get/put/post
- zmtelemetry/zmu: Daniel Caujolle-Bert refactors using ZoneMinder::Config, ZM_PATH_UNAME, lc()/chomp()/qx, eq instead of ==
- a2enmod: Postinst fix from
cgitorewrite
- QP-encode plain-text email body so URLs with
%EP%/%EPS%/%EPI%substitution tags survive transit (mail clients had been QP-decoding=NNdigit pairs)
Miscellaneous
- Advanced RtspServer pin to a0715995 (correct RTP marker bit from frame.last; cmake_minimum_required to 3.10)
- Don't pass null as the first parameter to strtotime() (PHP 8.1 deprecation)
- Added ZoneMinder.spec from the OBS project
Platform-Specific Changes
FreeBSD
- Fixed top command parsing (tested on FreeBSD 13.5)
- Check for /proc/meminfo before reading (does not exist on FreeBSD)
- Removed unnecessary kFreeBSD arch checks (was amd64/i386 only)
- FreeBSD arm build fixes
CI/CD
- Updated deb and aarch64 deb package workflows for the release-1.38 branch
- Renamed proposed rsync target to proposed-1.38
- Use
-r=<tag>for release builds instead of-s=CURRENT - Dynamic branch and deploy targets in deb package workflows
- RPM workflows fire on release-1.38 and derive deploy target from ref
- Package workflows now also fire on tag pushes so stable repo deploys actually run
- ESLint workflow updated to v9 for flat config support
- ESLint config synced to ESLint 9 flat config format
- Bumped GitHub Actions: download-artifact v8, upload-artifact v7, crazy-max/ghaction-import-gpg v7
Upgrade Notes
- Reverse-proxy users with AUTH_HASH_IPS enabled: Authentication now consults HTTP_X_FORWARDED_FOR (falling back to REMOTE_ADDR), so the per-monitor auth hash matches across PHP and C++ when ZoneMinder sits behind a proxy. If you carried custom ZoneMinder-side workarounds for this (e.g. disabling AUTH_HASH_IPS, custom auth_relay code), those can now be removed. Apache/nginx-side RemoteIPHeader / proxy_set_header configuration should stay as-is.
- Role-based permissions: Users who receive camera permissions via Roles will now correctly get live stream access through zms — previously the C++ User class only checked direct user permissions.
- PHP 8 users: GDImage memory handling is now PHP 8-aware. No action needed, but resource usage should be lower.
- Debian/Ubuntu fresh installs: postinst now adds www-data to the
videogroup and runsa2enmod rewrite(notcgi). Existing installs may want to verify these manually. - ONVIF subscription leaks: If cameras were previously stuck in NotAuthorized loops after a few hours, this should now self-recover without a zmc restart.
Contributors
This release includes contributions from the ZoneMinder development team and community members who reported bugs, tested fixes, and provided feedback.
Full Changelog: 1.38.1...1.38.2