dbConnect() turns emulated prepares off, so every query of the web UI, the
ajax calls and the includes the API runs (config, servers, users,
permissions) is a server-side prepare: prepare, execute and close, three
round trips to the database where one would do. From PHP 8.1, pdo_mysql
returns integers and floats as PHP ints and floats with emulated prepares
too, so values keep the types they have now. Emulate prepares there and keep
server-side prepares on older PHP, where every value would become a string.
Emulated prepares quote parameters bound as strings, which breaks LIMIT ?.
dbQuery() now binds integer values as integers (everything else as strings,
as execute() did), and the log and watch ajax calls cast their validated
offset and limit to int. With emulation, PDO would also run several
statements given in one string; MYSQL_ATTR_MULTI_STATEMENTS is turned off so
that is refused as before. CakePHP's own connection is not changed: it
already uses PDO's default, emulated prepares.
tests/php/test_database_emulated_prepares.php compares every column of every
table read through both kinds of prepare (types and values), checks
aggregates, LIMIT with positional and named integers, string and null
parameters, and that two statements in one string are refused.
The Log view was the only way to read a log, and its toolbar, its
bootstrap-table and the script behind them were written into the view, so
a second view could only have a log by copying all three.
Move the markup to skins/classic/includes/logpanel.php as
getLogPanelHTML(), and the behaviour to skins/classic/js/logpanel.js,
which picks up every .logPanel in the page and drives it. The script
keeps its request state, its selection and its in-flight XHR in a
closure, so two panels on one page no longer share them; the table gets
its ajax function from that closure instead of the global that
data-ajax="ajaxRequest" resolved to. Options cover what the call sites
differ on: the element id prefix, a fixed set of components, the page
size and whether the Back and Refresh page buttons are shown.
A panel locked to components sends embedded=1, and ajax/log.php then
leaves zmLogComponent and zmLogFilterLevel alone, so an embedded panel
cannot overwrite what the user chose in the Log view. Such a panel also
drops the component selector and column, and the unfiltered total, which
counts every log row and means nothing once the panel is scoped.
The level colours and the table's column rules move from the per-view
log.css files to the matching skin.css, since they now apply wherever a
panel is embedded.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firefox reports an error location as "line:column", for example 1500:28.
ajax/log.php passed that through validInt(), which is
preg_replace('/[^\-\d]/', '', $input)
so it only removes the colon and splices the two numbers into 150028.
Logs.Line is smallint(5) unsigned, max 65535, so the insert died with
PDOException SQLSTATE[22003]: Out of range value for column 'Line'
includes/logger.php:438 <- ajax/log.php:81
and the request returned 500. The endpoint failed on the very error it
was sent to report, so a js error surfaced as "AJAX execution error:
Internal Server Error [500]" instead of a log entry. Any Firefox error
past roughly line 655 hits this, on any view.
Parse the leading line number instead and clamp it to what the column
holds. validInt() is left alone; it is used widely and its callers rely
on the strip-everything behaviour, the defect is handing it a line:column
string.
Checked with an assert script over 1500:28, 1789:8, a plain 1500, an
over-range 70000 and 99999:1, empty, non-numeric, and array input; the
first three give the line, the next two clamp to 65535, the rest stay NULL.
Convert the Level and Component filter dropdowns on the system log view
into Chosen multi-selects so several values can be filtered at once. An
empty selection means "All" (shown via the placeholder), replacing the
former explicit All option.
- log.php: emit filterLevel[]/filterComponent[] with multiple + placeholder,
read the remembered selection as an array (tolerating the legacy scalar),
and drop the leftover ZM\Debug dumps of the component list
- js/log.js: send level/Component as arrays when non-empty; drop the
redundant filterComponent change binding now that data-on-change wires it
- ajax/log.php: match with Level IN (...) / Component IN (...) using
parameterized placeholders, and persist the selections as arrays in the
session
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address review feedback on the log create endpoint hardening:
- Coerce every client field to a scalar before sanitizing, so non-scalar
input (e.g. message[]=x) can no longer make preg_replace return an array
and trip a TypeError in logPrint().
- Strip control characters from level too, and log the sanitized value in
the "Unexpected logger level" message instead of the raw field, closing
the same control-character injection via the level field. Also stop
mutating $_POST; use a local variable.
- Replace the invalid-request print_r($_POST, true) dump, which emitted the
raw POST (with embedded newlines) into the log, with a fixed message.
refs #2466
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The log create endpoint (request=log&task=create) passed the client
supplied message and file straight to logPrint. logPrint only strips
trailing newlines, so embedded newlines survived into the flat text log
file and could forge what looked like additional log lines.
Strip control characters (newlines, carriage returns, tabs, etc.) from
message and file in createRequest before logging, matching the apparent
original intent (logPrint carries a comment expecting newlines to already
be replaced). A log message is a single line and the Log view stores it
as one Message column, so this has no effect on legitimate use.
This is defense-in-depth: the endpoint already requires an authenticated
System:Edit user (ZM_LOG_INJECT defaults off), the component is hardcoded
to web_js, and the Log view escapes angle brackets so the field cannot
inject markup. Relates to the long-standing log injection report.
refs #2466
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Open and close the session only once per page refresh.
First, save all changes in the $newSessionValue array, then open the session, set the new session values, and close the session.
Only write the correct $_REQUEST['level'] values to "zmLogFilterLevel" (if they are in ZM\Logger::$codes).
Earlier commits in this branch dropped reflected user input from
ajaxError() messages entirely to close Snyk XSS findings, but that
lost useful diagnostic information ("Unrecognised action <name>",
"Insufficient permissions for user <user>"). Use validHtmlStr()
(htmlspecialchars with ENT_QUOTES) so the values still appear in
the message and Snyk recognises the sanitization. Affects
add_monitors.php, device.php, event.php, events.php, log.php.
Four sites in log.php concatenated user-controlled data into the
$message string that flows to ajaxError() / exit(). Three permission
messages reflected \$user->Username(); the catch-all task message
reflected \$_REQUEST['task']. Drop the reflected tokens; still log
the unrecognised task server-side via ZM\\Warning.
We don't need an additional check to include the WHERE clause in the query string, since $where will only be populated if filters are specified.
Previously, there was an issue with counting the total number of pages when using a filter by components or levels.