mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2026-10-25 15:58:24 -04:00
fix: harden web interface against injection and SSRF vulnerabilities
FilterTerm.php: - Replace eval() with safe compare() method for SystemLoad, DiskPercent, and DiskBlocks filter conditions (RCE via crafted op/val) - Validate operator against allowlist in constructor - Sanitize collate field to alphanumeric/underscore only (SQLi) onvifprobe.php: - Use escapeshellarg() on interface, device_ep, soapversion, username, and password arguments passed to execONVIF() (command injection) Event.php: - Use escapeshellarg() on all arguments to zmvideo.pl instead of escapeshellcmd() on the whole command (command injection via format) - Anchor scale regex with ^ and $ to prevent partial matches image.php: - Restrict proxy URL scheme to http/https only (SSRF via file:// etc) filterdebug.php: - Use already-sanitized $fid instead of raw $_REQUEST['fid'] (XSS) MonitorsController.php: - Use escapeshellarg() on token, username, password, and monitor id in zmu shell command instead of escapeshellcmd() on whole command HostController.php: - Use escapeshellarg() on path in du command (command injection via mid) - Remove space from daemon name allowlist (argument injection) EventsController.php: - Remove single quotes from interval expression regex (SQLi) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
684b0ab38b
commit
ffe6362dc3
8 files changed
+60
-53
No files matched your search
@@ -16,7 +16,7 @@
|
||||
if ($fid) {
|
||||
$filter = new ZM\Filter($fid);
|
||||
if (!$filter->Id()) {
|
||||
echo '<div class="error">Filter not found for id '.$_REQUEST['fid'].'</div>';
|
||||
echo '<div class="error">Filter not found for id '.$fid.'</div>';
|
||||
}
|
||||
} else {
|
||||
$filter = new ZM\Filter();
|
||||
|
||||
@@ -396,7 +396,7 @@ class EventsController extends AppController {
|
||||
}
|
||||
$matches = NULL;
|
||||
$value = preg_replace('/^\s?interval\s?/i', '', $value);
|
||||
if (preg_match('/^(?P<expr>[ -.:0-9\']+)\s+(?P<unit>[_a-z]+)$/i', trim($value), $matches) !== 1) {
|
||||
if (preg_match('/^(?P<expr>[ \-.:0-9]+)\s+(?P<unit>[_a-z]+)$/i', trim($value), $matches) !== 1) {
|
||||
throw new Exception('Invalid interval: ' . $value);
|
||||
}
|
||||
$expr = trim($matches['expr']);
|
||||
@@ -429,7 +429,7 @@ class EventsController extends AppController {
|
||||
$matches = NULL;
|
||||
// https://dev.mysql.com/doc/refman/5.5/en/expressions.html#temporal-intervals
|
||||
// Examples: `'1-1' YEAR_MONTH`, `'-1 10' DAY_HOUR`, `'1.999999' SECOND_MICROSECOND`
|
||||
if (preg_match('/^(?P<expr>[ -.:0-9\']+)\s+(?P<unit>[_a-z]+)$/i', trim($interval), $matches) !== 1) {
|
||||
if (preg_match('/^(?P<expr>[ \-.:0-9]+)\s+(?P<unit>[_a-z]+)$/i', trim($interval), $matches) !== 1) {
|
||||
throw new Exception('Invalid interval: ' . $interval);
|
||||
}
|
||||
$expr = trim($matches['expr']);
|
||||
|
||||
@@ -44,7 +44,7 @@ class HostController extends AppController {
|
||||
return;
|
||||
}
|
||||
# To try to prevent abuse here, we are only going to allow certain characters in the daemon and args.
|
||||
$safe_daemon = preg_replace('/[^A-Za-z0-9\- \.]/', '', $daemon, -1, $count);
|
||||
$safe_daemon = preg_replace('/[^A-Za-z0-9\-\.]/', '', $daemon, -1, $count);
|
||||
if ($count) Error("Invalid characters found in daemon string ($daemon). Potential attack?");
|
||||
$safe_command = preg_replace('/[^a-z]/', '', $command, -1, $count);
|
||||
if ($count) Error("Invalid characters found in command string ($command). Potential attack?");
|
||||
@@ -240,8 +240,8 @@ class HostController extends AppController {
|
||||
|
||||
if ( $mid ) {
|
||||
// Get disk usage for $mid
|
||||
ZM\Debug("Executing du -s0 $zm_dir_events/$mid | awk '{print $1}'");
|
||||
$usage = shell_exec("du -s0 $zm_dir_events/$mid | awk '{print $1}'");
|
||||
ZM\Debug("Executing du -s0 $zm_dir_events/$mid | awk '{print \$1}'");
|
||||
$usage = shell_exec("du -s0 ".escapeshellarg($zm_dir_events.'/'.$mid)." | awk '{print \$1}'");
|
||||
} else {
|
||||
$monitors = $this->Monitor->find('all', array(
|
||||
'fields' => array('Id', 'Name', 'WebColour')
|
||||
|
||||
@@ -259,7 +259,7 @@ class MonitorsController extends AppController {
|
||||
global $user;
|
||||
$mToken = $this->request->query('token') ? $this->request->query('token') : $this->request->data('token');;
|
||||
if ($mToken) {
|
||||
$auth = ' -T '.$mToken;
|
||||
$auth = ' -T '.escapeshellarg($mToken);
|
||||
} else if (ZM_AUTH_RELAY == 'hashed') {
|
||||
$auth = ' -A '.calculateAuthHash(''); # Can't do REMOTE_IP because zmu doesn't normally have access to it.
|
||||
} else if (ZM_AUTH_RELAY == 'plain') {
|
||||
@@ -278,13 +278,17 @@ class MonitorsController extends AppController {
|
||||
$password = $_SESSION['password'];
|
||||
}
|
||||
|
||||
$auth = ' -U ' .$user->Username().' -P '.$password;
|
||||
$auth = ' -U '.escapeshellarg($user->Username()).' -P '.escapeshellarg($password);
|
||||
} else if (ZM_AUTH_RELAY == 'none') {
|
||||
$auth = ' -U ' .$user->Username();
|
||||
$auth = ' -U '.escapeshellarg($user->Username());
|
||||
}
|
||||
}
|
||||
|
||||
$shellcmd = escapeshellcmd(ZM_PATH_BIN."/zmu $verbose -m$id $q $auth");
|
||||
|
||||
$shellcmd = ZM_PATH_BIN.'/zmu'
|
||||
.($verbose ? " $verbose" : '')
|
||||
.' -m'.escapeshellarg($id)
|
||||
." $q"
|
||||
.$auth;
|
||||
$status = exec($shellcmd, $output, $rc);
|
||||
ZM\Debug("Command: $shellcmd output: ".implode(PHP_EOL, $output)." rc: $rc");
|
||||
if ($rc) {
|
||||
|
||||
@@ -700,22 +700,20 @@ class Event extends ZM_Object {
|
||||
}
|
||||
|
||||
function createVideo($format, $rate, $scale, $transform, $overwrite=false) {
|
||||
$command = ZM_PATH_BIN.'/zmvideo.pl -e '.$this->{'Id'}.' -f '.$format.' -r '.sprintf('%.2F', ($rate/RATE_BASE));
|
||||
if (preg_match('/\d+x\d+/', $scale)) {
|
||||
$command .= ' -S '.$scale;
|
||||
$command = ZM_PATH_BIN.'/zmvideo.pl -e '.escapeshellarg($this->{'Id'})
|
||||
.' -f '.escapeshellarg(preg_replace('/[^\w]/', '', $format))
|
||||
.' -r '.escapeshellarg(sprintf('%.2F', ($rate/RATE_BASE)));
|
||||
if (preg_match('/^\d+x\d+$/', $scale)) {
|
||||
$command .= ' -S '.escapeshellarg($scale);
|
||||
} else {
|
||||
if ( version_compare(phpversion(), '4.3.10', '>=') )
|
||||
$command .= ' -s '.sprintf('%.2F', ($scale/SCALE_BASE));
|
||||
else
|
||||
$command .= ' -s '.sprintf('%.2f', ($scale/SCALE_BASE));
|
||||
$command .= ' -s '.escapeshellarg(sprintf('%.2F', ($scale/SCALE_BASE)));
|
||||
}
|
||||
if ($transform != '') {
|
||||
$transform = preg_replace('/[^\w=]/', '', $transform);
|
||||
$command .= ' -t '.$transform;
|
||||
$command .= ' -t '.escapeshellarg($transform);
|
||||
}
|
||||
if ($overwrite)
|
||||
$command .= ' -o';
|
||||
$command = escapeshellcmd($command);
|
||||
$result = exec($command, $output, $status);
|
||||
Debug("generating Video $command: result($result outptu:(".implode("\n", $output )." status($status");
|
||||
return $status ? '' : rtrim($result);
|
||||
|
||||
+31
-31
@@ -40,6 +40,12 @@ class FilterTerm {
|
||||
$this->attr = preg_replace('/[^A-Za-z0-9\.]/', '', $this->attr, -1, $count);
|
||||
if ($count) Error("Invalid characters removed from filter attr {$term['attr']}, possible hacking attempt.");
|
||||
$this->op = isset($term['op']) ? $term['op'] : '=';
|
||||
$valid_ops = array('=', '!=', '>=', '<=', '>', '<', 'LIKE', 'NOT LIKE', '=~', '!~',
|
||||
'=[]', '![]', 'IN', 'NOT IN', 'EXISTS', 'IS', 'IS NOT');
|
||||
if (!in_array($this->op, $valid_ops)) {
|
||||
Warning('Invalid operator in filter term: ' . $this->op);
|
||||
$this->op = '=';
|
||||
}
|
||||
$this->val = isset($term['val']) ? $term['val'] : '';
|
||||
if (is_array($this->val)) $this->val = implode(',', $this->val);
|
||||
if ( isset($term['cnj']) ) {
|
||||
@@ -71,7 +77,7 @@ class FilterTerm {
|
||||
}
|
||||
$this->cookie = isset($term['cookie']) ? $term['cookie'] : '';
|
||||
$this->placeholder = isset($term['placeholder']) ? $term['placeholder'] : null;
|
||||
$this->collate = isset($term['collate']) ? $term['collate'] : '';
|
||||
$this->collate = isset($term['collate']) ? preg_replace('/[^a-zA-Z0-9_]/', '', $term['collate']) : '';
|
||||
$this->multiple = isset($term['multiple']) ? $term['multiple'] : '';
|
||||
$this->chosen = isset($term['chosen']) ? $term['chosen'] : '';
|
||||
|
||||
@@ -80,6 +86,21 @@ class FilterTerm {
|
||||
}
|
||||
} # end function __construct
|
||||
|
||||
private function compare($left, $op, $right) {
|
||||
$right = floatval($right);
|
||||
switch ($op) {
|
||||
case '=': return $left == $right;
|
||||
case '!=': return $left != $right;
|
||||
case '>': return $left > $right;
|
||||
case '>=': return $left >= $right;
|
||||
case '<': return $left < $right;
|
||||
case '<=': return $left <= $right;
|
||||
default:
|
||||
Warning("Invalid operator '$op' in compare");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
# Returns an array of values. AS term->value can be a list, we will break it apart, remove quotes etc
|
||||
public function sql_values() {
|
||||
$values = array();
|
||||
@@ -433,16 +454,9 @@ class FilterTerm {
|
||||
}
|
||||
} # end foreach Storage Area
|
||||
} else if ( $this->attr == 'SystemLoad' ) {
|
||||
$string_to_eval = 'return getLoad() '.$this->op.' '.$this->val.';';
|
||||
try {
|
||||
$ret = eval($string_to_eval);
|
||||
Debug("Evaled $string_to_eval = $ret");
|
||||
if ( $ret )
|
||||
return true;
|
||||
} catch ( Throwable $t ) {
|
||||
Error('Failed evaluating '.$string_to_eval);
|
||||
return false;
|
||||
}
|
||||
$ret = $this->compare(getLoad(), $this->op, $this->val);
|
||||
Debug("SystemLoad compare: getLoad() {$this->op} {$this->val} = " . ($ret ? 'true' : 'false'));
|
||||
if ($ret) return true;
|
||||
} else {
|
||||
Error('testing unsupported pre term ' . $this->attr);
|
||||
}
|
||||
@@ -459,27 +473,13 @@ class FilterTerm {
|
||||
return !file_exists($event->Path());
|
||||
}
|
||||
} else if ( $this->attr == 'DiskPercent' ) {
|
||||
$string_to_eval = 'return $event->Storage()->disk_usage_percent() '.$this->op.' '.$this->val.';';
|
||||
try {
|
||||
$ret = eval($string_to_eval);
|
||||
Debug("Evalled $string_to_eval = $ret");
|
||||
if ( $ret )
|
||||
return true;
|
||||
} catch ( Throwable $t ) {
|
||||
Error('Failed evaluating '.$string_to_eval);
|
||||
return false;
|
||||
}
|
||||
$ret = $this->compare($event->Storage()->disk_usage_percent(), $this->op, $this->val);
|
||||
Debug("DiskPercent compare: " . ($ret ? 'true' : 'false'));
|
||||
if ($ret) return true;
|
||||
} else if ( $this->attr == 'DiskBlocks' ) {
|
||||
$string_to_eval = 'return $event->Storage()->disk_usage_blocks() '.$this->op.' '.$this->val.';';
|
||||
try {
|
||||
$ret = eval($string_to_eval);
|
||||
Debug("Evalled $string_to_eval = $ret");
|
||||
if ( $ret )
|
||||
return true;
|
||||
} catch ( Throwable $t ) {
|
||||
Error('Failed evaluating '.$string_to_eval);
|
||||
return false;
|
||||
}
|
||||
$ret = $this->compare($event->Storage()->disk_usage_blocks(), $this->op, $this->val);
|
||||
Debug("DiskBlocks compare: " . ($ret ? 'true' : 'false'));
|
||||
if ($ret) return true;
|
||||
} else if ( $this->attr == 'Tags' ) {
|
||||
// Debug('TODO: Complete this post_sql_condition for Tags val: ' . $this->val . ' op: ' . $this->op . ' id: ' . $this->id);
|
||||
// Debug(print_r($this, true));
|
||||
|
||||
@@ -50,7 +50,7 @@ function execONVIF($cmd) {
|
||||
|
||||
function probeCameras($localIp) {
|
||||
$cameras = array();
|
||||
$lines = @execONVIF('probe 1.1,1.2'.(isset($_REQUEST['interface']) ? ' '.$_REQUEST['interface'] : '' ));
|
||||
$lines = @execONVIF('probe 1.1,1.2'.(isset($_REQUEST['interface']) ? ' '.escapeshellarg($_REQUEST['interface']) : '' ));
|
||||
if ($lines) {
|
||||
foreach ($lines as $line) {
|
||||
$line = rtrim($line);
|
||||
@@ -93,7 +93,7 @@ function probeCameras($localIp) {
|
||||
|
||||
function probeProfiles($device_ep, $soapversion, $username, $password) {
|
||||
$profiles = array();
|
||||
if ($lines = @execONVIF("profiles $device_ep $soapversion $username $password")) {
|
||||
if ($lines = @execONVIF('profiles '.escapeshellarg($device_ep).' '.escapeshellarg($soapversion).' '.escapeshellarg($username).' '.escapeshellarg($password))) {
|
||||
foreach ($lines as $line) {
|
||||
$line = rtrim( $line );
|
||||
|
||||
|
||||
@@ -60,6 +60,11 @@ if (!empty($_REQUEST['proxy'])) {
|
||||
}
|
||||
|
||||
$url_parts = parse_url($url);
|
||||
if (!$url_parts || !isset($url_parts['scheme']) ||
|
||||
!in_array(strtolower($url_parts['scheme']), array('http', 'https'))) {
|
||||
ZM\Warning('Image proxy only supports http/https URLs');
|
||||
return;
|
||||
}
|
||||
$username = $url_parts['user'];
|
||||
$password = isset($url_parts['pass']) ? $url_parts['pass'] : '';
|
||||
|
||||
|
||||
Reference in new issue
Block a user