#4914 unconditionally drops Javascript errors and CSP violations sourced from
browser extensions. Some operators want to know when a plugin is touching the
ZoneMinder tab, so gate the suppression behind a config entry.
Add ZM_LOG_BROWSER_EXTENSIONS (boolean, default no) in the logging category.
It is exposed to JS by the existing non-private-config loop in skin.js.php as
the string '0'/'1' (the same convention as ZM_LOG_INJECT), so logger.js only
filters extension sources when it is '0'. Default behaviour is unchanged:
extension noise stays out of the log.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When an event closes the recording file is renamed from incomplete.* to
<Id>-video.* and the DB row is updated. A stale Event model still reports
DefaultVideo=incomplete.mp4, producing spurious 'File does not exist'
warnings. When the incomplete file is missing, clear the object cache and
re-read the event from the database to check the finished file before
warning.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address PR #4931 review feedback:
- monitor.js ControlEdit_onClick: parse ControlId as an integer and only
navigate when it is a positive integer, instead of URL-encoding the raw
select value.
- skin.js .zmlink handler: drop the unused binding from the catch clause.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
generateAuthHash() cached every hash under the IP-keyed slot
'AuthHash'.$remoteAddr, but the value was IP-bound only when $useRemoteAddr was
set. getZmuCommand() calls generateAuthHash(false, true), which wrote an
IP-less hash into the IP-bound slot and reset AuthHashGeneratedAt. Because the
status poll runs getZmuCommand (web/ajax/status.php) right before emitting the
auth hash, the poll then served the IP-less hash to the browser; the next
IP-bound request was rejected by the validator, redirecting the user to login
roughly every poll. This happens with a completely stable client IP, so it is
distinct from the IP-rotation case.
Key the cache slot by the address actually baked into the value: only use the
session address when this caller asked for it (and AUTH_HASH_IPS is on). IP-less
and IP-bound hashes now occupy separate slots and can no longer clobber each
other.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Primary change: remove web/js/hls-1.6.13/hls-demo.js (+ its source map),
the upstream demo bundle. It is referenced nowhere in the web interface
and is the source of four CodeQL alerts (js/xss-through-dom,
js/incomplete-sanitization, js/redos: #262-#265).
Optional cleanup (can be dropped from this PR if maintainers prefer to
keep the full dist): the directory also shipped alternate builds that the
web interface never loads. The interface only loads hls.min.js
(watch.php, cycle.php, montage.php). Also removed:
- hls.js, hls.light.*, hls.mjs, hls.light.mjs (+ maps): alternate
full/light/ESM builds; we use the full minified UMD build
- hls.worker.js (+ map): standalone transmuxer worker. hls.min.js inlines
the worker via a Blob URL and only loads an external worker when
config.workerPath is set; ZM never sets it, so it is never fetched
- hls.d.mts, hls.d.ts, hls.js.d.ts: TypeScript declarations, dev-only
Keeps hls.min.js and its source map. The demo removal alone clears
#262-#265; trimming the unused builds additionally clears the
js/insecure-randomness alerts in them (#258-#261).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolves three CodeQL alerts in ZoneMinder-authored JavaScript:
- skin.js .zmlink click handler (js/xss-through-dom, #135): navigated to
the element's href/data-url via location.assign() without checking the
scheme, so a javascript:/data: URL in the attribute would run on click.
Skip navigation for javascript:/data:/vbscript: URLs.
- skin.js strip_html() (js/incomplete-multi-character-sanitization #266,
js/polynomial-redos #267): stripped tags with /<[^>]+>/g, which is an
incomplete sanitizer and can backtrack. Parse the string with DOMParser
and return textContent instead -- correct and linear. Callers in
devices/reports/snapshots/controlcaps/events.js use it to extract a
plain Id from a rendered cell, behaviour preserved.
- monitor.js ControlEdit_onClick (js/xss-through-dom, #246): encode the
select value with encodeURIComponent() before building the controlcap
URL.
Verified: eslint clean on both files, node --check passes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The tablename field of a filter term was copied verbatim into SQL by
sql_attr(), while attr, op, val and collate were all sanitized. An
authenticated user with Events View permission could inject SQL via the
filter[Query][terms][N][tablename] request parameter, enabling blind
read access to the whole database (password hashes, camera credentials).
Restrict tablename to the table aliases actually used by the filter
queries (E, M, S, F, T, ET, Snapshots); reject anything else, log it,
and fall back to 'E'.
Refs GHSA-q2w3-h644-f8xq
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CakePHP applies the datasource 'encoding' as SET NAMES, and it was 'utf8',
MySQL's 3-byte utf8mb3 alias. Like the C++ daemon connection, this mangles
4-byte UTF-8 characters in utf8mb4 columns such as Monitors.Name to '?' on
read and truncates them on write, so the API returned and stored corrupted
names. Set it to utf8mb4 to match the schema.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Monitors with Function=None have no Monitor_Status row, so the LEFT JOIN
to Monitor_Status leaves S.Status NULL and the condition Status='NotRunning'
never matched them - selecting the NotRunning status filter returned no
monitors. Coalesce a missing status to NotRunning in the filter condition,
mirroring the NULL->NotRunning handling already used for display in
console.php.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- API (database.php.default): only set the PDO verify flag when SSL is
actually configured (ZM_DB_SSL_CA_CERT set), matching the web/Perl/C++
layers. Previously a fresh install's default (1) would set the flag on a
non-SSL connection, since the CakePHP datasource merges 'flags' uncondi-
tionally.
- Both PHP layers: cast to string and trim before parsing the value, and use
strict in_array, to avoid type-juggling and stray-whitespace edge cases.
- zm_db.cpp: use my_bool (not char) for the MYSQL_OPT_SSL_VERIFY_SERVER_CERT
fallback argument, the type libmysqlclient expects. That branch only
compiles on older clients without MYSQL_OPT_SSL_MODE, where my_bool exists.
refs #3816
Addresses review feedback on #4223:
- The IS NOT Odd/Even branch emitted '% 2 = ' (identical to IS), so
'MaxScore IS NOT Odd' matched odd values instead of even. Emit '% 2 != '
so the negation is correct.
- Reword the comment to reflect that IS is only preserved for NULL here;
any other value (including TRUE/FALSE) compares for equality.
refs #4223
A filter term such as 'Score IS 2' or 'Score IS NOT 2' emitted literal
SQL 'Score IS 2', which is invalid - IS / IS NOT only accept
NULL/TRUE/FALSE in MySQL. Keep IS/IS NOT only when the value is NULL
(and the existing Odd/Even modulo handling); for any other value emit
= / != so the term is valid and matches as the user intended.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a ZM_DB_SSL_VERIFY_SERVER_CERT setting so a database connection that uses
ZM_DB_SSL_CA_CERT can talk to a server with a self-signed or otherwise
non-matching certificate. When enabled, verification is by identity (the cert
must chain to the CA and its CN/SAN must match ZM_DB_HOST), consistent across
the C++ daemons, the PHP web interface, the CakePHP API and the Perl scripts.
This re-does the reverted #3817. That PR broke the build because it called
mysql_options(MYSQL_OPT_SSL_VERIFY_SERVER_CERT, ...), and that enum was removed
from the MySQL 8.0 C client in favour of MYSQL_OPT_SSL_MODE; it also passed a
c_str() where a my_bool* was expected, and referenced the PHP constant
unconditionally (fatal on PHP 8 for an upgraded install whose zm.conf predates
the option).
The option that controls server-cert verification differs by client library and
the symbols are enum values, not macros, so CMake feature-detects them by
compiling:
- HAVE_MYSQL_OPT_SSL_MODE (MySQL 5.7.11+/8.0, MariaDB Connector/C 3.1+)
- HAVE_MYSQL_OPT_SSL_VERIFY_SERVER_CERT (older MariaDB/MySQL)
zm_db.cpp uses SSL_MODE_VERIFY_IDENTITY / SSL_MODE_REQUIRED when the former is
available, else falls back to the latter with a proper my_bool.
Value handling is three-way in every layer: a truthy value verifies, a false-y
value (0/false/no/off) skips verification, and an empty/unset value leaves the
client default in place so existing installs are unchanged on upgrade. PHP, the
API datasource (via PDO flags) and the Perl DSN are all guarded with defined()
checks. Fresh installs default to 1.
Documents the full ZM_DB_* connection and SSL settings, including the hostname
verification gotcha when connecting by IP, in docs/userguide/configfiles.rst.
refs #3816
The ul.tabList rules in the base, classic and dark skin stylesheets
match no elements. Nothing in the codebase sets class="tabList"; the
former tabbed-heading navigation it styled is gone. The only remaining
'tabList' token is the role="tabList" ARIA attribute on the controlcap
pills list, which the ul.tabList class selector never matches.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The monitors index unconditionally LEFT JOINed Groups_Monitors and collapsed
the duplicate rows (monitors in multiple groups) with GROUP BY `Monitor`.`Id`.
That GROUP BY fails under ONLY_FULL_GROUP_BY on engines without functional
dependency detection (MariaDB), raising 1055 'Monitor.Name isn't in GROUP BY',
so /api/monitors.json returned a 500 while /api/monitors/<id>.json worked.
Only join Groups_Monitors when the request filters by group (matching the
existing EventsController pattern and the original commented-out intent), drop
the GROUP BY, and dedupe by monitor Id in the existing result loop to cover
multi-value GroupId filters. Portable across MySQL and MariaDB.
refs #3633