mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2026-10-29 09:48:32 -04:00
Two places in ZoneMinder run a command the operator typed: a filter with "Execute command", and the scheduled tasks added for #4957. Both run it as the web user through a shell, which is close to shell access on the server. Until now the only control over that was who may edit, and the answer was anyone with System rights. ZM_ALLOWED_COMMANDS names the programs that may be run, comma separated. Empty is the default and places no restriction, so an upgrade changes nothing and the filters people already have keep working; setting it narrows both features at once. Only the program is matched, because an exact whole-command match could never work for filters: zmfilter.pl appends the event path to whatever the filter stored, so no two runs have the same command line. Arguments are therefore free, which means the program being on the list has to be the whole of the guarantee -- so while a list is in force a command may not contain the characters that start another program: semicolon, pipe, ampersand, backtick and dollar-paren. Without that, "/usr/local/bin/backup.sh; anything" would pass on the strength of its first word. Redirection is deliberately allowed; it starts no program, and refusing it would break the ordinary "... >> /var/log/mine.log". Refused in three places, because each answers a different question. The filter and scheduled task editors refuse on save, so the operator is told at the time rather than by a log line days later. zmfilter.pl refuses at the moment it runs, because a row can reach the database without passing through the web ui and because the setting may have been narrowed since. And cronRebuild leaves a newly disallowed task out of the crontab, so tightening the setting takes jobs away rather than waiting for somebody to edit each one. The rule exists twice, in web/includes/Command.php and ZoneMinder::General, since one is PHP and the other Perl. Rather than hope they stay in step, tests/php/test_allowed_commands.php puts every case to both and fails if they disagree. That earned itself immediately: both split the command on whitespace before handling quotes, so a program path containing a space was cut in half and compared as the wrong string. The cross-check found it in both at once. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
74 lines
2.6 KiB
PHP
74 lines
2.6 KiB
PHP
<?php
|
|
namespace ZM;
|
|
|
|
require_once('database.php');
|
|
require_once('Object.php');
|
|
require_once('CronTab.php');
|
|
require_once('Command.php');
|
|
|
|
// A command ZoneMinder puts in the web user's crontab for cron to run.
|
|
//
|
|
// The counterpart to a scheduled MonitorAction: that one runs a device
|
|
// command ZoneMinder already knows how to build, this one runs whatever the
|
|
// operator says. Both end up as lines in the same marked block, written by
|
|
// cronRebuild().
|
|
class CronJob extends ZM_Object {
|
|
protected static $table = 'CronJobs';
|
|
|
|
protected $defaults = array(
|
|
'Id' => null,
|
|
'Name' => '',
|
|
'Enabled' => 1,
|
|
'Schedule' => '',
|
|
'Command' => '',
|
|
'ServerId' => null,
|
|
);
|
|
|
|
public static function find($parameters = array(), $options = array()) {
|
|
return ZM_Object::_find(self::class, $parameters, $options);
|
|
}
|
|
|
|
public static function find_one($parameters = array(), $options = array()) {
|
|
return ZM_Object::_find_one(self::class, $parameters, $options);
|
|
}
|
|
|
|
public function Server() {
|
|
if (!$this->{'ServerId'}) return null;
|
|
require_once('Server.php');
|
|
return Server::find_one(array('Id'=>$this->{'ServerId'}));
|
|
}
|
|
|
|
// Whether this job belongs in the crontab of the server running this code.
|
|
// A job with no server named belongs on every server, which is what a single
|
|
// server install wants and gets without having to say so.
|
|
public function belongsOnThisServer() {
|
|
if (!$this->{'ServerId'}) return true;
|
|
if (!defined('ZM_SERVER_ID') or !ZM_SERVER_ID) return true;
|
|
return ($this->{'ServerId'} == ZM_SERVER_ID);
|
|
}
|
|
|
|
// The crontab line, or null when this job is not one to write.
|
|
//
|
|
// Validated again here rather than trusted from the row: the database is
|
|
// not the boundary, the crontab is, and a row could have been written by
|
|
// something that did not check.
|
|
public function cronLine() {
|
|
if (!$this->{'Enabled'}) return null;
|
|
if (!cronScheduleIsValid($this->{'Schedule'})) return null;
|
|
if (!cronCommandIsValid($this->{'Command'})) return null;
|
|
// Asked again on every rebuild, so narrowing ZM_ALLOWED_COMMANDS drops
|
|
// the jobs it no longer covers out of the crontab rather than leaving
|
|
// them running until somebody edits each one.
|
|
if (!commandAllowedByConfig($this->{'Command'})) {
|
|
Warning('Scheduled task '.$this->{'Id'}.' is not in ZM_ALLOWED_COMMANDS, leaving it out of the crontab');
|
|
return null;
|
|
}
|
|
|
|
$line = trim($this->{'Schedule'}).' '.trim($this->{'Command'});
|
|
// Named so somebody reading the crontab can find the row it came from.
|
|
$line .= ' # ZoneMinder job '.intval($this->{'Id'});
|
|
return $line;
|
|
}
|
|
}
|
|
?>
|