github ZoneMinder/zoneminder 1.38.2
Seek and Destroy 1.38.2

5 hours ago

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-jwt from 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-, and bytes=-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 +1 instead of +last_duration for 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; #volumeControls now 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 .$file concatenation error

Database & Filters

  • Filter::Sql() now clears accumulated state (PostSQLConditions, HasDiskPercent, HasDiskBlocks, HasSystemLoad) before rebuilding — previously grew unboundedly
  • Filter::Execute uses prepare instead of prepare_cached since 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 cgi to rewrite

Email

  • QP-encode plain-text email body so URLs with %EP%/%EPS%/%EPI% substitution tags survive transit (mail clients had been QP-decoding =NN digit 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

  1. 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.
  2. 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.
  3. PHP 8 users: GDImage memory handling is now PHP 8-aware. No action needed, but resource usage should be lower.
  4. Debian/Ubuntu fresh installs: postinst now adds www-data to the video group and runs a2enmod rewrite (not cgi). Existing installs may want to verify these manually.
  5. 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

Don't miss a new zoneminder release

NewReleases is sending notifications on new releases.