Neutron, our AI engine, scored 97.1% on UC Berkeley's CyberGym benchmark. Learn more

Deep Dive

Deep Dive

Cronicle Task Scheduler Privilege Escalation (CVE-2026-85076)

A non-admin Cronicle user can inject environment variables through event params and run code in another job's context. Fixed in 0.9.124.

TL;DR

  • A non-admin Cronicle user with event privileges can add undeclared keys to an event's params object. The job runner uppercases those keys into the child process environment, so the user can override LD_PRELOAD, PATH, NODE_OPTIONS and similar variables.
  • Cronicle before 0.9.124 is affected. The 0.9.124 release validates param keys against the plugin schema. Later releases, through 0.9.144, add further override and job-API hardening.
  • The GitHub CVE record rates it CVSS v4.0 9.0 Critical, while the repository advisory rates it High. No exploitation in the wild is reported, and the CVE is not in CISA KEV as of 2026-11-07.
  • Upgrade to 0.9.144. If you cannot, restrict event creation and execution to trusted administrators and audit stored events; see "How do I fix it?".

Am I affected?

You are exposed only when all of these conditions hold:

  • The server runs Cronicle before 0.9.124.
  • The attacker holds a Cronicle account that is not an administrator.
  • That account can create, edit or run events. The advisory names the run_events, create_events and edit_events privileges.
  • The attacker can reach the Cronicle API or web interface.

Cronicle is a multi-server task scheduler with a web front end. It runs jobs as the user configured for the plugin or event. A non-admin who can create events can already run commands as that user. This flaw adds the ability to change the environment of a job that another user or plugin owns, including variables that load code before the job starts.

Branch Affected versions Fixed version
Cronicle every release before 0.9.124 0.9.124 (2026-07-17); 0.9.144 (2026-11-07) adds further hardening

The CVE record lists every release before 0.9.124 as affected. The 0.9.124 release is the fix for this CVE. The 0.9.141 through 0.9.144 releases harden related event and job override paths, so run 0.9.144.

Check the running version with the control script:

/opt/cronicle/bin/control.sh version

In a container, run the same command inside it:

docker exec <container> /opt/cronicle/bin/control.sh version

Any version before 0.9.124 is affected.

How do I fix it?

  1. Back up the Cronicle data directory and configuration.
  2. Run /opt/cronicle/bin/control.sh upgrade 0.9.144 on each server. Without a version argument, the command upgrades to the latest stable release. The upgrade keeps the data, users and configuration.
  3. For a container, pull the new image and recreate the container. Pulling the image alone does not replace a running container.
  4. Confirm the new version with /opt/cronicle/bin/control.sh version.
  5. In a cluster, repeat the upgrade on every server. Cronicle requires the same configuration on all servers.

If you cannot upgrade, restrict event creation and execution to administrators you trust. The advisory names run_events, create_events and edit_events as the privileges that reach the flaw. Removing those privileges from non-admin roles closes the documented path, but it also removes a feature those users rely on.

Audit the stored events for param keys that the selected plugin does not define. A key such as ld_preload, node_options or bash_env is not part of a normal plugin schema. The advisory publishes no vendor workaround beyond the upgrade.

What is the root cause?

The job runner copies every key in an event's params object into the child process environment, and the event API validates only that params is an object. Nothing checks the keys against the plugin's declared parameters.

In lib/job.js at 0.9.123, the job copies the event params:

job.params = event.params || {};

Later, when the child process starts, the runner uppercases each key and writes it into the environment:

// add plugin params as env vars, expand $INLINE vars (unless escaped)
if (job.params) {
    for (var key in job.params) {
        child_opts.env[key.toUpperCase()] =
            (''+job.params[key]).replace(/(\\?)\$(\w+)/g, function(m_all, m_g1, m_g2) {
            return m_g1 ? ('$' + m_g2) : ((m_g2 in child_opts.env) ? child_opts.env[m_g2] : '');
        });
    }
}

The API accepts the event object after a type check only:

if (("params" in event) && (typeof(event.params) != 'object')) {
    this.doError('api', "Malformed event parameter: params (must be object)", callback);
    return false;
}

The admin-controlled plugin params are applied first. The event params are applied after them, so a key that collides with an admin-set variable wins. The runner does not check that the key belongs to the plugin.

The params object is also stored on the event when an attacker creates or updates it. That makes the injection persistent for events the attacker can edit. For run_event, the params apply to that run only.

How is it exploited?

  1. The attacker signs in with a non-admin account that can create, edit or run events.
  2. The attacker creates an event, or updates an existing event, and adds an undeclared key to params. The advisory's example uses ld_preload with a path to a shared library.
  3. The attacker runs the event, or waits for its schedule. The job runner uppercases the key and writes it into the child environment. ld_preload becomes LD_PRELOAD, and the dynamic loader loads the attacker's library before the job's own code.
  4. The attacker can also use node_options, bash_env or pythonpath. The advisory's two-step example writes a Node.js payload with one event, then runs a second event as a more privileged user with node_options set to --require=/tmp/evil.js.
  5. The injected code runs with the target job's user and access. The advisory describes the impact as privilege escalation, arbitrary code execution, data access or service disruption, depending on the plugin and runtime.

The advisory publishes a request that creates an event with an extra param. Adapted from the advisory, the request body has this shape:

POST /api/app/create_event/v1
Content-Type: application/json

{
  "session_id": "USER_SESSION_ID",
  "title": "Legitimate Event",
  "enabled": 1,
  "category": "general",
  "target": "allgrp",
  "plugin": "shellplug",
  "params": {
    "script": "#!/bin/sh\necho hello",
    "ld_preload": "/tmp/evil.so"
  }
}

The ld_preload key is not defined by the plugin. The runner still turns it into LD_PRELOAD for the job.

How does the attack flow?

A non-admin adds a key the plugin never defined, the job runner turns that key into an environment variable, and the target job loads attacker code before it starts.

flowchart LR A["Non-admin creates or runs an event"] --> B["params includes ld_preload or node_options"] B --> C["API checks only that params is an object"] C --> D["job.params copies every key"] D --> E["job.js uppercases each key into child_opts.env"] E --> F["Target job starts with LD_PRELOAD or NODE_OPTIONS"] F --> G["Attacker code runs as the job user"]

What can an attacker do?

A non-admin with event privileges can run code in the context of another event's job. The impact depends on the target plugin's user and on what the job can reach.

The conditions are:

  • The attacker has a non-admin Cronicle account.
  • The account can create, edit or run events.
  • The target event uses a plugin that runs with more access than the attacker has, or the attacker needs to change that job's environment.
  • The attacker can reach the API or the web interface.
  • For a persistent injection, the attacker can update the target event. For a one-off injection, the attacker can run it with modified params.

The advisory rates the flaw High and describes the impact as privilege escalation, arbitrary code execution, data access or service disruption. The GitHub CVE record rates it CVSS v4.0 9.0 Critical. The difference comes from the scoring model and the assumed prerequisites; both are stated here.

The flaw does not by itself give the Cronicle administrator account or the root user. The injected code runs as the job's configured user. A job that runs as root, or a plugin that starts a root process, gives the attacker that access. The advisory does not demonstrate a specific root path.

Why does the fix work?

The fix validates every event param key against the plugin's declared parameters and copies only declared params into the job. Cronicle's patch adds a requireValidEventParams() helper and changes the job runner to use the plugin schema.

The new helper in lib/api.js:

requireValidEventParams: function(params, plugin, callback) {
    // Only allow Event params that are defined by the admin-controlled Plugin.
    // This prevents arbitrary param keys from becoming child environment variables.
    var allowed_params = Object.create(null);
    if (plugin.params) plugin.params.forEach( function(param) {
        allowed_params[ param.id ] = true;
    } );

    for (var key in params) {
        if (!Object.prototype.hasOwnProperty.call(params, key)) continue;
        if (!allowed_params[key]) {
            this.doError('event', "Unknown Plugin parameter: " + key, callback);
            return false;
        }
    }

    return true;
},

The job runner now builds job.params from the plugin schema:

-               job.params = event.params || {};
                job.now = event.now || Tools.timeNow(true);
                job.event_title = event.title;
                job.plugin_title = plugin.title;
...
-               // plugin params may have changed outside of event, 
-               // so recopy missing / hidden ones
+               // Only allow params defined by the admin-controlled Plugin schema,
+               // so arbitrary Event / API keys cannot become child environment variables.
+               var supplied_params = Tools.isaHash(event.params) ? event.params : {};
+               job.params = {};
+               
+               // Plugin params may have changed outside of the Event, so recopy
+               // missing values, and always use the trusted value for hidden params.
                if (plugin.params) plugin.params.forEach( function(param) {
-                   if (!(param.id in job.params) || (param.type == 'hidden')) {
+                   if (
+                       (param.type != 'hidden') &&
+                       Object.prototype.hasOwnProperty.call(supplied_params, param.id)
+                   ) {
+                       job.params[ param.id ] = supplied_params[ param.id ];
+                   }
+                   else {
                        job.params[ param.id ] = param.value;
                    }
                } );

After the fix, an undeclared key is rejected at the API and is not copied into the environment. A hidden plugin param always takes the plugin's value, so an event cannot override it.

The 0.9.141 through 0.9.144 releases add related fixes. The 0.9.144 release note says "Prevent script parameter overrides in run_event". The 0.9.143 release restricts the job update APIs to a strict property allowlist. These are defense in depth for the same trust boundary.

How do I check if I was compromised?

Start with the running version. A server before 0.9.124 is exposed.

/opt/cronicle/bin/control.sh version

Inspect the stored schedule for events whose params contain keys the plugin does not define. The storage keeps the schedule in the Cronicle data directory. A small Node.js check can list the keys:

node -e 'const fs=require("fs"),p="/opt/cronicle/data/global/schedule";for(const f of fs.readdirSync(p)){const j=JSON.parse(fs.readFileSync(p+"/"+f));for(const e of j.items||[]){const k=Object.keys(e.params||{});if(k.some(x=>/^(ld_preload|node_options|bash_env|pythonpath|path)$/i.test(x)))console.log(e.id,e.title,k.join(","));}}'

A hit is a lead, not proof. A plugin can legitimately define a parameter with one of these names. Compare the keys with the plugin's schema.

Check the job logs and the Cronicle log for child environments or errors that name an unexpected library or Node.js require. The runner logs child spawn options at debug level 9. If debug logging is enabled, search for LD_PRELOAD, NODE_OPTIONS or the path of a file you do not recognize.

grep -Ei 'LD_PRELOAD|NODE_OPTIONS|BASH_ENV|PYTHONPATH' /opt/cronicle/logs/*.log 2>/dev/null

The vendor publishes no indicators of compromise. A failed attempt may leave no trace, and a successful attempt may leave only the job output or a changed event.

Timeline

  • 2026-04-03: the earlier advisory GHSA-5j3v-cq96-xw6v for update_event job-output injection is published and fixed in 0.9.111.
  • 2026-07-17: commit da0228fd and release 0.9.124 validate event params against the plugin schema.
  • 2026-09-02: CVE-2026-85076 is reserved.
  • 2026-09-04: advisory GHSA-9w42-v8cj-68gq is published.
  • 2026-11-05: the CVE record is published at 20:28 UTC.
  • 2026-11-07: releases 0.9.143 and 0.9.144 add further override and job-API hardening.

The fix shipped seven weeks before the advisory and four months before the CVE record.
Figure 1: From the 0.9.124 fix to the advisory, the CVE record and the November hardening releases. Source: Cronicle releases, the advisory and the CVE record, checked November 7, 2026

FAQ

Who can exploit this flaw?

A Cronicle user who is not an administrator but can create, edit or run events. The advisory names the run_events, create_events and edit_events privileges. An anonymous visitor cannot exploit it.

Does it give root access?

Not by itself. The injected environment applies to the job's child process, which runs as the user configured for the plugin or event. A job configured to run as root gives the attacker root. A job that runs as an unprivileged user gives that user's access.

Is 0.9.124 enough?

It fixes CVE-2026-85076. The 0.9.141 through 0.9.144 releases add further hardening of the same event and job override paths, including "Prevent script parameter overrides in run_event" in 0.9.144. Run 0.9.144.

Was it exploited in the wild?

No exploitation in the wild is reported as of 2026-11-07, and the CVE is not in CISA KEV. The advisory publishes request examples, so the technique is public.

What if I cannot upgrade right away?

Remove the run_events, create_events and edit_events privileges from non-admin roles. Audit stored events for param keys the plugin does not define. The restriction removes a feature from those users, so plan the change with them.

How do I know which plugin an event uses?

The event record has a plugin field that names the plugin id. The plugin definition lists its params. Compare the event's param keys with that list. The check command in "How do I check if I was compromised?" prints the event id, title and keys.

References

  • GitHub advisory GHSA-9w42-v8cj-68gq, published 2026-09-04, with the vulnerable code, the reproduction steps and the fix: https://github.com/jhuckaby/Cronicle/security/advisories/GHSA-9w42-v8cj-68gq (checked November 7, 2026)
  • CVE-2026-85076 record, published 2026-11-05, CVSS v4.0 9.0 and CWE-863/CWE-74: https://www.cve.org/CVERecord?id=CVE-2026-85076 (checked November 7, 2026)
  • Cronicle commit da0228fd, "Harden event parameter validation and plugin handling", 2026-07-17: https://github.com/jhuckaby/Cronicle/commit/da0228fdea9bc57c09876c00ea0feda70a0ef110 (checked November 7, 2026)
  • Cronicle release 0.9.124, 2026-07-17: https://github.com/jhuckaby/Cronicle/releases/tag/v0.9.124 (checked November 7, 2026)
  • Cronicle release 0.9.144, 2026-11-07, with the "Prevent script parameter overrides in run_event" note: https://github.com/jhuckaby/Cronicle/releases/tag/v0.9.144 (checked November 7, 2026)
  • Cronicle release 0.9.143, 2026-11-07, with the job update API allowlist: https://github.com/jhuckaby/Cronicle/releases/tag/v0.9.143 (checked November 7, 2026)
  • Cronicle lib/job.js at v0.9.123, the param-to-environment copy: https://github.com/jhuckaby/Cronicle/blob/v0.9.123/lib/job.js#L537 (checked November 7, 2026)
  • Cronicle lib/api.js at v0.9.123, the params type check: https://github.com/jhuckaby/Cronicle/blob/v0.9.123/lib/api.js#L252 (checked November 7, 2026)
  • Cronicle lib/job.js at v0.9.124, the schema-based param copy: https://github.com/jhuckaby/Cronicle/blob/v0.9.124/lib/job.js#L227 (checked November 7, 2026)
  • Cronicle command-line documentation, /opt/cronicle/bin/control.sh version and upgrade: https://github.com/jhuckaby/Cronicle/blob/v0.9.123/docs/CommandLine.md (checked November 7, 2026)
  • GitHub, jhuckaby/Cronicle, 5,875 stars and 509 forks: https://github.com/jhuckaby/Cronicle (checked November 7, 2026)
  • Docker Hub, cronicle/cronicle, 132,076 pulls: https://hub.docker.com/r/cronicle/cronicle (checked November 7, 2026)
  • CISA Known Exploited Vulnerabilities catalog JSON feed, checked for CVE-2026-85076 on November 7, 2026 (not listed): https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json (checked November 7, 2026)