2 Commits
Author SHA1 Message Date
Isaac ConnorandClaude Opus 5 78f389cdea feat: add ZM_ALLOWED_COMMANDS to restrict what filters and tasks may run refs #4957
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>
2026-10-28 18:01:25 -05:00
Isaac ConnorandClaude Opus 5 f90ada1958 feat: add a UI for scheduled tasks, backed by the crontab refs #4957
This is the rest of what #4957 asked for -- run Perl, PHP, bash and binaries
on a flexible schedule, from a visual interface -- and it needs no daemon,
because cron is the daemon. 1.39.37 already taught ZoneMinder to rewrite a
marked block in the web user's crontab for scheduled monitor actions while
leaving every line outside that block alone. A CronJobs row is another line in
the same block.

So the parts that would otherwise have to be written and then supervised --
timing, catch-up, restart on boot, running as a known user -- are cron's, and
have been since 1975. What ZoneMinder adds is the table, the page, and the
rule that the crontab is a projection of the table: rebuilt whenever a row
changes, never the other way round, so the two cannot drift.

The schedule uses the same pickers the monitor actions got, reading "Every
Monday at 7pm", with the cron field beside them for anything the pickers
cannot say. The pickers build the spec in the browser so the field fills in as
you choose, and the server builds and validates it again on save, because the
browser is not what keeps a crontab line honest. The two mappings are checked
against each other in tests/js/cron-schedule.test.js, which reads the PHP map
rather than restating it.

The command is not judged on what it does; running things is the point. It is
judged on whether it stays on its own line and means in cron what the operator
meant: a newline is refused because it would end the entry and let the rest
become a line with a schedule of its own, and an unescaped % is refused
because cron stops the command there and feeds the remainder in as stdin,
which would silently run less than was typed. Cron's own escape, \%, is
accepted.

Editing takes System edit rights, and every add, change and delete is logged
with the command. Anyone who can edit here can run commands as the web user,
which is worth saying plainly -- though it is the same trust Filters'
AutoExecuteCmd has carried for years, not a new one.

ServerId NULL means every server writes the job into its own crontab, which
is what a single server install wants without having to say so; naming a
server restricts it to that one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-28 18:01:25 -05:00