Back to blog

The crontab-ui CVEs: A Web UI for Your Crontab Is a Remote Shell

Two critical CVEs in crontab-ui let anyone who can reach it write cron entries and run commands. How to tell if you are exposed, lock it down, and catch crontab drift.

CronGuard TeamCron Job Monitoring Experts
6 min read
Close-up of a black screen showing a green ubuntu@ubuntu:~$ shell prompt followed by a cursor, with the left edge of the text out of focus

Two critical CVEs in a cron dashboard

crontab-ui is a small Node.js app that puts a web page in front of a crontab. You add, edit and pause jobs from a browser instead of over SSH.

On 10 August 2026 two CVEs were published against it: CVE-2026-72589 and CVE-2026-72590. Both score 9.8 under CVSS 3.1. Both affect every version up to and including 0.4.2, which is still the latest release on npm (published 20 April 2026). Neither CVE record lists a fixed version. If you run crontab-ui, you are running a vulnerable build.

What it comes down to: anyone who can reach the web port, and get past basic auth if you turned it on, can write lines into your crontab. A crontab line is a command the system runs on a schedule. So this is code execution.

What crontab-ui does with your input

Neither bug is clever.

CVE-2026-72590: newlines in env_vars

The GET /crontab handler passes the env_vars query parameter to set_crontab(), which appends it to the crontab string and adds a newline. Nothing strips newlines from the value first. The CVE is filed as CWE-93, CRLF injection.

Put a URL-encoded %0A in env_vars and the variable line ends. Whatever follows becomes its own line, schedule and command included. And it is a GET request. No form, no body.

CVE-2026-72589: importing a poisoned database

The import button exists to restore backups of the job database. POST /import writes the uploaded file over the live database and reloads it. It does not look at what is inside.

makeCommand() then builds each crontab line by gluing strings together. The job's command goes in as-is, and the optional hook field is spliced into a shell snippet that runs after the job. A crafted import turns into shell commands the next time the crontab is written. This one is CWE-78, OS command injection.

Why the Docker image makes it worse

The CVE records say the attacker is unauthenticated. For a lot of installs that is accurate.

Authentication is opt-in

The auth middleware only switches on when both BASIC_AUTH_USER and BASIC_AUTH_PWD are set. Leave either one out and every route is open, /crontab and /import included. No startup warning. No default password.

The image listens on every interface

Started with plain node, app.js binds to 127.0.0.1 unless you set HOST. The project's Dockerfile sets ENV HOST=0.0.0.0, which a container needs, and the README quick start is this:

docker run -d -p 8000:8000 alseambusher/crontab-ui

That publishes port 8000 on every interface of the host, with no credentials. The image also runs crond under supervisord, so injected entries execute in that container with whatever volumes and network access you handed it.

Checking whether you are exposed

Checking from the host

Look for the container and for anything on the default port:

docker ps --format '{{.Names}}  {{.Image}}  {{.Ports}}' | grep -i crontab
ss -ltnp | grep ':8000'

0.0.0.0:8000->8000/tcp means every interface. 127.0.0.1:8000->8000/tcp means only the host itself can reach it.

Checking from outside

From a machine on a different network, fetch the root page and read the status code:

curl -s -o /dev/null -w '%{http_code}\n' http://your-server:8000/

A 200 means the dashboard loads without a login, and both CVEs are open to anyone. A 401 means basic auth is on. Fewer people can exploit the bugs then. They are still there.

Locking it down until a fix ships

Bind to loopback and tunnel in

Stop publishing the port. Bind to loopback and use an SSH tunnel when you need the UI:

docker run -d -p 127.0.0.1:8000:8000 \
  -e BASIC_AUTH_USER=ops -e BASIC_AUTH_PWD="$(openssl rand -base64 24)" \
  alseambusher/crontab-ui

# from your laptop
ssh -L 8000:127.0.0.1:8000 you@your-server

Turn on auth and add TLS

Set both auth variables. Always. Basic auth over plain HTTP sends the password with every request, so use the SSL_CERT and SSL_KEY variables from the README or put a TLS-terminating proxy in front. Anyone holding the password can still use the injection paths.

Or remove it

Honestly, this is the option we would pick. If two people touch three jobs a month, a crontab file in git, deployed by the tooling you already have, does the same work and listens on no port at all.

Watching the crontab itself

Why a heartbeat monitor misses an added job

A dead man's switch alerts when a success signal stops arriving. It catches "the backup stopped running." It has nothing to say about "someone added a line." The injected entry never checks in, your real jobs keep pinging, and the dashboard stays green.

A drift check that reports in

So make the crontab itself check in. Keep a known-good copy in version control and run a small job that compares the live crontab with it. Ping the monitor only when they match:

#!/bin/bash
set -euo pipefail

BASELINE=/etc/cron-baseline/root.crontab

if crontab -l -u root | diff -q - "$BASELINE" > /dev/null; then
  curl -fsS -m 10 --retry 3 https://cronguard.app/api/ping/your-monitor-id > /dev/null
fi

Run it every 15 minutes from somewhere the web UI cannot edit, a systemd timer on the host for example. With crontab-ui in a container, the crontab it writes lives inside that container, so compare that one. Any drift stops the ping and the monitor alerts on the silence. Deleting the check produces the same alert.

Frequently asked questions about the crontab-ui vulnerabilities

Which versions of crontab-ui are affected? CVE-2026-72589 and CVE-2026-72590 both affect every version up to and including 0.4.2. That is the latest release on npm and neither CVE record lists a fixed version, so treat any current install as vulnerable.

Does setting a password fix the crontab-ui vulnerabilities? No. Setting BASIC_AUTH_USER and BASIC_AUTH_PWD keeps anonymous visitors out, since without both variables every route is open. The injection bugs are still there for anyone who has the password.

Why is the Docker deployment the riskiest setup? The Dockerfile sets HOST to 0.0.0.0 and the README quick start publishes port 8000 without credentials. Run that on a server with a public address and you have an unauthenticated crontab editor on the internet.

How do I check if my crontab-ui is reachable? Read the container port mapping: 0.0.0.0:8000 means every interface, 127.0.0.1:8000 means local only. From an outside machine, a 200 on the root page means no login is required and a 401 means basic auth is on.

Can heartbeat monitoring detect an injected cron job? Not by itself, because a dead man's switch alerts on missing check-ins and an injected job sends none. Add a scheduled drift check that compares the live crontab with a baseline in version control and pings the monitor only when they match. Any change then becomes silence, and silence becomes an alert.

Further reading


Conclusion: crontab-ui writes whatever an HTTP request hands it into your crontab, and as of today there is no patched release. Take it off the network or take it out. Then give the crontab its own heartbeat, so the day it changes without anyone approving it is the day you hear about it.

Sources: CVE-2026-72589, CVE-2026-72590, and the crontab-ui repository.

Share

Related posts

Black-and-white close-up of a rack-mounted blade server chassis, its perforated blade bays marked with fan status labels and power ratings, with thick cables looped across the front
Best PracticesCron Job Security: Hardening Scheduled Tasks Against Common Attacks
Rows of patch panels in a server rack with looped black network cables, lit in deep green and half hidden behind the dark edge of a neighbouring cabinet
ReliabilityGang-Scheduled CronJobs in Kubernetes 1.37: The Run That Never Starts
Three server cabinets with mesh doors in a dark room, packed with equipment, orange and blue cables looping down the front and rows of small green status lights
ReliabilityWhen the OOM Killer Takes Down Cron Itself, Not Just Your Job

Set up your first monitor.
It'll take 30 seconds.