Forums Bug Reports Thread

Cron 'command' field accepts arbitrary trailing CLI arguments with no validation (role-4 over tenant-scope jobs)

Patrick Bass · Jun 6 · 13 · 1 Locked
[Minor] [Normal Priority] [Bug Fixed] [Always Reproduces]
🚀 OP Jun 6, 2026 8:20pm

Area: Admin deep-dive (trust/safety) (audit p15a) · Surface: POST /admin/cron/{id}/edit + POST /admin/cron/{id}/run (AdminCronController@update / runNow) · Dimension: security · Severity: minor

A tenant admin (role 4) can rewrite a tenant-scope cron job's command to any in-tree bin/*.php script (the runner guard at adhoc-cron-runner.sh:57 limits the script to bin/*.php, which mitigates running non-bin files) WITH attacker-chosen CLI arguments, then fire it via run-now. Several bin scripts take destructive flags (e.g. purge-*.php --confirm); arbitrary argument injection lets the admin trigger privileged maintenance actions with parameters the UI never intended to expose. Impact is bounded (actor is already a tenant admin, scripts limited to bin/*.php by the runner), so severity is minor, but the controller validates neither the bin/ prefix nor the argument list. OWASP A01/A08 (insufficient validation of an operator-controlled execution surface).

Evidence

AdminCronController.php:282-339 update(): a role-4 tenant admin may edit any scope!='system' job (loadJobForMutation only blocks system-scope for <role 5). The only command check is `is_file($scriptAbs)` on the first whitespace-split token (lines 326-331); the rest of the command string — arbitrary trailing arguments — is stored verbatim into cron_jobs.command. runNow() (lines 412-414) re-uses the same first-token-only is_file check, then queues the job; bin/adhoc-cron-runner.sh:81 executes `php ${PROJECT_ROOT}/${COMMAND}` with the full command (args included).

Suggested fix. In update(): enforce the same allow-list the runner uses (command must match `^bin/[A-Za-z0-9._-]+\.php`), and validate/whitelist trailing arguments (or forbid free-text args for tenant-scope jobs). Keep the controller's accepted command set in sync with adhoc-cron-runner.sh's bin/*.php guard so the controller rejects what the runner would refuse rather than silently queuing a run that fails as 'unsafe command'.

Filed by the automated tenant-app audit and adversarially evidence-verified. Status: verified. Open — not yet actioned.


Patrick Bass
@mobieus

🚀 Jun 7, 2026 5:49am

Resolved — fixed and deployed. Commit ea9f0311e960, shipped dev-first then to all tenants on 2026-06-06.

In update(), replaced the loose is_file()-only check with an allow-list matching bin/adhoc-cron-runner.sh's bin/*.php guard: script path must match ^bin/[A-Za-z0-9._-]+\.php$ (restricted charset also blocks ../ traversal) and trailing args are restricted to ^[A-Za-z0-9._/=,\-\s]+$ (no shell metacharacters), since the runner expands ${PROJECT_ROOT}/${COMMAND} unquoted into a shell. Both regexes use /D to block CRLF/argv injection. Closing the write path means runNow (which reads only from the registry row) can never receive an unsafe command. php -l passes.

Status: fixed. Thread closed and locked.


Patrick Bass
@mobieus

Log in or register to reply to this thread.