mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2026-11-02 10:44:30 -05: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>
102 lines
3.6 KiB
PHP
102 lines
3.6 KiB
PHP
<?php
|
|
//
|
|
// ZoneMinder web action file
|
|
// Copyright (C) 2026 ZoneMinder LLC
|
|
//
|
|
// This program is free software; you can redistribute it and/or
|
|
// modify it under the terms of the GNU General Public License
|
|
// as published by the Free Software Foundation; either version 2
|
|
// of the License, or (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program; if not, write to the Free Software
|
|
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
//
|
|
|
|
// Scheduled task edit actions.
|
|
//
|
|
// These rows become lines in the web user's crontab and run as that user, so
|
|
// this is a System-level privilege and is gated as one. That is the same trust
|
|
// Filters' AutoExecuteCmd has carried for years, not a new one, but it is
|
|
// worth being plain about: anyone who can edit here can run commands as the
|
|
// web user.
|
|
if (!canEdit('System')) {
|
|
ZM\Warning('Need System permissions to manage scheduled tasks');
|
|
return;
|
|
}
|
|
|
|
require_once('includes/CronJob.php');
|
|
require_once('includes/CronTab.php');
|
|
require_once('includes/Command.php');
|
|
|
|
$cron_error = '';
|
|
|
|
if ($action == 'save') {
|
|
$posted = isset($_REQUEST['newJob']) && is_array($_REQUEST['newJob'])
|
|
? $_REQUEST['newJob'] : array();
|
|
|
|
$schedule = isset($posted['Schedule']) ? trim($posted['Schedule']) : '';
|
|
$command = isset($posted['Command']) ? trim($posted['Command']) : '';
|
|
$name = isset($posted['Name']) ? trim($posted['Name']) : '';
|
|
|
|
// Both are checked here rather than only in the browser: this is the last
|
|
// point before the strings become a crontab line.
|
|
if (!cronScheduleIsValid($schedule)) {
|
|
$cron_error = translate('CronBadSchedule');
|
|
} else if (!cronCommandIsValid($command)) {
|
|
$cron_error = translate('CronBadCommand');
|
|
} else if (!commandAllowedByConfig($command)) {
|
|
$cron_error = commandRefusedMessage();
|
|
} else {
|
|
$values = array(
|
|
'Name' => $name !== '' ? $name : $command,
|
|
'Enabled' => empty($posted['Enabled']) ? 0 : 1,
|
|
'Schedule' => $schedule,
|
|
'Command' => $command,
|
|
'ServerId' => empty($posted['ServerId']) ? null : validInt($posted['ServerId']),
|
|
);
|
|
|
|
$id = empty($_REQUEST['id']) ? 0 : validInt($_REQUEST['id']);
|
|
$job = $id ? ZM\CronJob::find_one(array('Id'=>$id)) : new ZM\CronJob();
|
|
if (!$job) $job = new ZM\CronJob();
|
|
|
|
if ($job->Id()) {
|
|
$job->save($values);
|
|
ZM\Info('Updated scheduled task '.$job->Id().': '.$schedule.' '.$command);
|
|
} else if ($job->insert($values)) {
|
|
ZM\Info('Added scheduled task '.$job->Id().': '.$schedule.' '.$command);
|
|
} else {
|
|
$cron_error = $job->get_last_error();
|
|
}
|
|
}
|
|
} else if ($action == 'delete') {
|
|
if (!empty($_REQUEST['markIds'])) {
|
|
foreach ($_REQUEST['markIds'] as $id) {
|
|
$id = validInt($id);
|
|
if (!$id) continue;
|
|
ZM\Info('Deleting scheduled task '.$id);
|
|
dbQuery('DELETE FROM CronJobs WHERE Id=?', array($id));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (in_array($action, array('save', 'delete')) and ($cron_error === '')) {
|
|
// The crontab is a projection of the table, rebuilt whenever the table
|
|
// changes, so the two cannot drift.
|
|
if (cronRebuild() === false)
|
|
$cron_error = translate('CronWriteFailed');
|
|
}
|
|
|
|
if ($cron_error !== '') {
|
|
ZM\Error('Scheduled tasks: '.$cron_error);
|
|
$redirect = '?view=cron&error='.urlencode($cron_error);
|
|
} else {
|
|
$redirect = '?view=cron';
|
|
}
|
|
?>
|