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