mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2026-10-02 23:45:08 -04:00
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.
301 lines
11 KiB
PHP
301 lines
11 KiB
PHP
<?php
|
|
$data = array();
|
|
$message = '';
|
|
|
|
//
|
|
// INITIALIZE AND CHECK SANITY
|
|
//
|
|
|
|
// task must be set
|
|
if (!isset($_REQUEST['task'])) {
|
|
$message = 'This request requires a task to be set';
|
|
} else if ($_REQUEST['task'] == 'query') {
|
|
if (!canView('System')) {
|
|
$message = 'Insufficient permissions to view log entries for user '.validHtmlStr($user->Username());
|
|
} else {
|
|
$data = queryRequest();
|
|
}
|
|
} else if ($_REQUEST['task'] == 'create' ) {
|
|
global $user;
|
|
if (!$user or (!canEdit('System') and !ZM_LOG_INJECT)) {
|
|
$message = 'Insufficient permissions to create log entries for user '.validHtmlStr($user->Username());
|
|
} else {
|
|
createRequest();
|
|
}
|
|
} else if ($_REQUEST['task'] == 'delete') {
|
|
global $user;
|
|
if (!canEdit('System')) {
|
|
$message = 'Insufficient permissions to delete log entries for user '.validHtmlStr($user->Username());
|
|
} else {
|
|
if (!empty($_REQUEST['ids'])) {
|
|
$ids = array_map('intval', (array)$_REQUEST['ids']);
|
|
$placeholders = implode(',', array_fill(0, count($ids), '?'));
|
|
dbQuery('DELETE FROM Logs WHERE Id IN (' . $placeholders . ')', $ids);
|
|
}
|
|
}
|
|
} else {
|
|
// Only the query and create tasks are supported at the moment
|
|
$message = 'Unrecognised task '.validHtmlStr($_REQUEST['task']);
|
|
}
|
|
|
|
if ($message) {
|
|
ajaxError($message);
|
|
return;
|
|
}
|
|
ajaxResponse($data);
|
|
|
|
//
|
|
// FUNCTION DEFINITIONS
|
|
//
|
|
|
|
function createRequest() {
|
|
// Every field here is attacker-controlled. Coerce each to a scalar and strip
|
|
// control characters (newlines, carriage returns, tabs, etc.) so nothing can
|
|
// forge additional lines in the text log file, and so non-scalar input (e.g.
|
|
// message[]=x) cannot trip a TypeError in logPrint(). A log message is a
|
|
// single line; the Log view stores it as one Message column regardless.
|
|
$sanitize = function($value) {
|
|
return is_scalar($value) ? preg_replace('/[\x00-\x1F\x7F]+/', ' ', (string)$value) : '';
|
|
};
|
|
|
|
$level = $sanitize($_POST['level'] ?? '');
|
|
$message = $sanitize($_POST['message'] ?? '');
|
|
// Strip the URL scheme/host from file, then the control characters.
|
|
$file = (isset($_POST['file']) && is_scalar($_POST['file']))
|
|
? $sanitize(preg_replace('/\w+:\/\/[\w.:]+\//', '', (string)$_POST['file']))
|
|
: '';
|
|
|
|
if ($level === '' || $message === '') {
|
|
ZM\Error('Invalid log create: level and message are required');
|
|
return;
|
|
}
|
|
|
|
ZM\logInit(array('id'=>'web_js'));
|
|
// Firefox reports the location as "line:column" (e.g. 1500:28). validInt()
|
|
// only strips non-digit characters, so it splices the two together into
|
|
// 150028, which overflows the smallint Logs.Line column and makes this
|
|
// endpoint die with a 500 -- the log request fails on the very error it was
|
|
// sent to report. Take the leading line number and clamp it to the column.
|
|
$line = NULL;
|
|
if (!empty($_POST['line']) and is_scalar($_POST['line'])
|
|
and preg_match('/\d+/', (string)$_POST['line'], $line_matches)) {
|
|
$line = min((int)$line_matches[0], 65535);
|
|
}
|
|
|
|
$levels = array_flip(ZM\Logger::$codes);
|
|
if (!isset($levels[$level])) {
|
|
ZM\Error('Unexpected logger level '.$level); // already sanitized above
|
|
$level = 'ERR';
|
|
}
|
|
ZM\Logger::fetch()->logPrint($levels[$level], $message, $file, $line);
|
|
}
|
|
|
|
function queryRequest() {
|
|
// Offset specifies the starting row to return, used for pagination
|
|
$offset = 0;
|
|
if (isset($_REQUEST['offset'])) {
|
|
if ((!is_int($_REQUEST['offset']) and !ctype_digit($_REQUEST['offset']))) {
|
|
ZM\Error('Invalid value for offset: ' . $_REQUEST['offset']);
|
|
} else {
|
|
$offset = $_REQUEST['offset'];
|
|
}
|
|
}
|
|
|
|
// Limit specifies the number of rows to return
|
|
$limit = 100;
|
|
if (isset($_REQUEST['limit'])) {
|
|
if ((!is_int($_REQUEST['limit']) and !ctype_digit($_REQUEST['limit']))) {
|
|
ZM\Error('Invalid value for limit: ' . $_REQUEST['limit']);
|
|
} else {
|
|
$limit = $_REQUEST['limit'];
|
|
}
|
|
}
|
|
// The table we want our data from
|
|
$table = 'Logs';
|
|
|
|
$nameMainQuery = 't1'; # To optimize queries using a subquery
|
|
$nameSubQuery = 't2';
|
|
|
|
// The names of the dB columns in the log table we are interested in
|
|
$columns = array('Id', 'TimeKey', 'Component', 'ServerId', 'Pid', 'Code', 'Message', 'File', 'Line');
|
|
$columnsContext = $columns;
|
|
// The names of columns shown in the log view that are NOT dB columns in the database
|
|
$col_alt = array('DateTime', 'Server');
|
|
|
|
$sort = 'TimeKey';
|
|
if (isset($_REQUEST['sort'])) {
|
|
$sort = $_REQUEST['sort'];
|
|
if ($sort == 'DateTime') $sort = 'TimeKey';
|
|
if ($sort == 'Server') $sort = 'ServerId';
|
|
}
|
|
if (!in_array($sort, array_merge($columns, $col_alt))) {
|
|
ZM\Error('Invalid sort field: ' . $sort);
|
|
return;
|
|
}
|
|
|
|
// Order specifies the sort direction, either asc or desc
|
|
$order = (isset($_REQUEST['order']) and (strtolower($_REQUEST['order']) == 'asc')) ? 'ASC' : 'DESC';
|
|
|
|
if ($nameMainQuery !== '' && $nameSubQuery !== '') {
|
|
array_walk($columnsContext, function(&$value, $key, $nameMainQuery) {
|
|
$value = $nameMainQuery . '.' . $value;
|
|
}, $nameMainQuery);
|
|
}
|
|
|
|
$col_str = implode(', ', $columns);
|
|
$col_str_context = implode(', ', $columnsContext);
|
|
$data = array();
|
|
$query = array();
|
|
$query['values'] = array();
|
|
$likes = array();
|
|
$where = '';
|
|
// There are two search bars in the log view, normal and advanced
|
|
// Making an exuctive decision to ignore the normal search, when advanced search is in use
|
|
// Alternatively we could try to do both
|
|
//
|
|
// Advanced search contains an array of "column name" => "search text" pairs
|
|
// Bootstrap table sends json_ecoded array, which we must decode
|
|
$advsearch = isset($_REQUEST['filter']) ? json_decode($_REQUEST['filter'], JSON_OBJECT_AS_ARRAY) : array();
|
|
// Search contains a user entered string to search on
|
|
$search = isset($_REQUEST['search']) ? $_REQUEST['search'] : '';
|
|
if (count($advsearch)) {
|
|
foreach ($advsearch as $col=>$text) {
|
|
if (!in_array($col, array_merge($columns, $col_alt))) {
|
|
ZM\Error("'$col' is not a searchable column name");
|
|
continue;
|
|
}
|
|
// Don't use wildcards on advanced search
|
|
//$text = '%' .$text. '%';
|
|
array_push($likes, $col.' LIKE ?');
|
|
array_push($query['values'], $text);
|
|
}
|
|
$where = '(' .implode(' OR ', $likes). ')';
|
|
|
|
} else if ($search != '') {
|
|
$search = '%' .$search. '%';
|
|
foreach ( $columns as $col ) {
|
|
array_push($likes, $col.' LIKE ?');
|
|
array_push($query['values'], $search);
|
|
}
|
|
$where = '(' .implode(' OR ', $likes). ')';
|
|
}
|
|
|
|
// Component is a multi-select: accept a scalar (legacy) or an array of
|
|
// component names and match any of them.
|
|
$requestComponents = array();
|
|
if (isset($_REQUEST['Component'])) {
|
|
foreach ((array)$_REQUEST['Component'] as $component) {
|
|
if (is_scalar($component) && (string)$component !== '') $requestComponents[] = (string)$component;
|
|
}
|
|
}
|
|
if (count($requestComponents)) {
|
|
if ($where) $where .= ' AND ';
|
|
$placeholders = implode(', ', array_fill(0, count($requestComponents), '?'));
|
|
$where .= 'Component IN (' . $placeholders . ')';
|
|
foreach ($requestComponents as $component) $query['values'][] = $component;
|
|
}
|
|
|
|
if (!empty($_REQUEST['ServerId'])) {
|
|
if ($where) $where .= ' AND ';
|
|
$where .= 'ServerId = ?';
|
|
$query['values'][] = $_REQUEST['ServerId'];
|
|
}
|
|
/* We have an indexed 'Level', not 'Code'.
|
|
if (!empty($_REQUEST['level'])) {
|
|
if ($where) $where .= ' AND ';
|
|
$where .= 'Code = ?';
|
|
$query['values'][] = $_REQUEST['level'];
|
|
}
|
|
*/
|
|
// Level is a multi-select: accept a scalar (legacy) or an array of level codes
|
|
// ('DBG', 'INF', ...). Keep only recognized codes and match any of them.
|
|
$level_codes = array_flip(ZM\Logger::$codes);
|
|
$requestLevels = array();
|
|
if (isset($_REQUEST['level'])) {
|
|
foreach ((array)$_REQUEST['level'] as $level) {
|
|
if (is_scalar($level) && isset($level_codes[(string)$level])) {
|
|
$requestLevels[(string)$level] = $level_codes[(string)$level];
|
|
}
|
|
}
|
|
}
|
|
if (count($requestLevels)) {
|
|
if ($where) $where .= ' AND ';
|
|
$placeholders = implode(', ', array_fill(0, count($requestLevels), '?'));
|
|
$where .= ' Level IN (' . $placeholders . ')';
|
|
foreach ($requestLevels as $code) $query['values'][] = $code;
|
|
}
|
|
|
|
if (!empty($_REQUEST['StartDateTime'])) {
|
|
$start_time = strtotime($_REQUEST['StartDateTime']);
|
|
if ($start_time) {
|
|
if ($where) $where .= ' AND ';
|
|
$where .= 'TimeKey >= ?';
|
|
$query['values'][] = $start_time;
|
|
} else {
|
|
ZM\Warning("Unable to parse StartDateTime ".$_REQUEST['StartDateTime']. " into a timestamp");
|
|
}
|
|
}
|
|
if (!empty($_REQUEST['EndDateTime'])) {
|
|
$end_time = strtotime($_REQUEST['EndDateTime']);
|
|
if ($end_time) {
|
|
if ($where) $where .= ' AND ';
|
|
$where .= 'TimeKey <= ?';
|
|
$query['values'][] = $end_time;
|
|
} else {
|
|
ZM\Warning("Unable to parse EndDateTime ".$_REQUEST['EndDateTime']. " into a timestamp");
|
|
}
|
|
}
|
|
|
|
zm_session_start();
|
|
$_SESSION['zmLogComponent'] = $requestComponents;
|
|
$_SESSION['zmLogFilterLevel'] = array_keys($requestLevels);
|
|
session_write_close();
|
|
|
|
if ($where) $where = ' WHERE '.$where;
|
|
|
|
$data['totalNotFiltered'] = dbFetchOne('SELECT count(*) AS Total FROM `' .$table.'`', 'Total');
|
|
if ($where) {
|
|
$data['total'] = dbFetchOne('SELECT count(*) AS Total FROM `' .$table.'` '.$where, 'Total', $query['values']);
|
|
} else {
|
|
$data['total'] = $data['totalNotFiltered'];
|
|
}
|
|
|
|
if ($nameMainQuery !== '' && $nameSubQuery !== '') { # Optimized query
|
|
$query['sql'] = '
|
|
SELECT ' .$col_str_context. '
|
|
FROM `' .$table. '` ' .$nameMainQuery. '
|
|
JOIN (
|
|
SELECT Id
|
|
FROM `'.$table.'` '.$where. '
|
|
ORDER BY ' .$sort. ' ' .$order. '
|
|
LIMIT ?, ?
|
|
) AS ' .$nameSubQuery. '
|
|
ON ' .$nameMainQuery. '.Id=' .$nameSubQuery. '.Id
|
|
ORDER BY ' .$nameMainQuery. '.' .$sort. ' ' .$order;
|
|
} else {
|
|
$query['sql'] = 'SELECT ' .$col_str. ' FROM `' .$table. '` ' .$where. ' ORDER BY ' .$sort. ' ' .$order. ' LIMIT ?, ?';
|
|
}
|
|
|
|
array_push($query['values'], $offset, $limit);
|
|
|
|
$rows = array();
|
|
$results = dbFetchAll($query['sql'], NULL, $query['values']);
|
|
|
|
global $dateTimeFormatter;
|
|
foreach ($results as $row) {
|
|
$row['DateTime'] = empty($row['TimeKey']) ? '' : $dateTimeFormatter->format(intval($row['TimeKey']));
|
|
$Server = $row['ServerId'] ? ZM\Server::find_one(array('Id'=>$row['ServerId'])) : null;
|
|
|
|
$row['Server'] = $Server ? $Server->Name() : '';
|
|
// Strip out all characters that are not ASCII 32-126 (yes, 126)
|
|
$row['Message'] = preg_replace('/[^\x20-\x7E]/', '', htmlspecialchars($row['Message']));
|
|
$row['File'] = preg_replace('/[^\x20-\x7E]/', '', strip_tags($row['File']));
|
|
$rows[] = $row;
|
|
}
|
|
$data['rows'] = $rows;
|
|
$data['logstate'] = logState();
|
|
$data['updated'] = $dateTimeFormatter->format(time());
|
|
|
|
return $data;
|
|
}
|