CVE-2026-45662

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

CVE-2026-45662: command injection in Dokploy through the registry URL on docker logout

Dokploy escaped the registry URL for docker login but not for docker logout, so deleting a crafted registry ran shell commands on the host.

For your decision

Impact
Deleting a registry can execute commands on the Dokploy host that controls Docker.
Conditions
An account must be able to configure and delete a registry. The write-up explains the storage path and its limits.
Recommended action
Install Dokploy 0.29.3 or later and review registry management permissions.

Advisory facts

Product
Dokploy
Package
npm · @dokploy/server
Affected versions
< 0.29.3
Fixed in
0.29.3
CVSS
8.8 High
Weakness
CWE-78
Access needed
Account required
Advisory published
Credit
rootzero

Investigation summary

When a registry was deleted, self-hosted Dokploy ran docker logout with the stored registry URL concatenated into a shell command, although the same file escaped that value for docker login. A user allowed to create and delete registries could store a URL containing shell metacharacters and run commands on the Dokploy host, which drives Docker and holds the platform’s secrets. Fixed in 0.29.3.

Escaped login, forgotten logout

We took an earlier Dokploy fix seriously: registry.ts already had shEscape() and a safe construction for docker login. So we looked for nearby commands handling the same registry URL. Further down the file, removeRegistry() put it directly into docker logout, executed by a helper based on child_process.exec(). Input validation required only a string.

We built the proof around a crafted URL retained in a registry record and read back when that record was deleted. The transactional detail mattered: creation could roll back if docker login failed, while registry.update stored the value before attempting login. docker logout on deletion then reached the shell. We reported the issue on 29 March 2026. Dokploy added escaping at execution and URL validation on 9 May, released 0.29.3 and CVE-2026-45662 on the 11th, then protected the remaining login commands in 0.29.8. A user able to manage registries is a prerequisite; this was not an anonymous request.

A shell command on each side of a registry record

Dokploy manages Docker registries on servers it controls. Adding a registry invokes docker login; deleting it invokes docker logout. Both commands are built in a Node process with access to the host’s Docker environment.

Most of what Dokploy does ends in a shell command. Its helper execAsync() is child_process.exec() wrapped in a promise, which means every command string passes through /bin/sh -c. Container registries are one of the features built this way: when a user adds a registry, Dokploy runs docker login, and when the user deletes it, Dokploy runs docker logout.

The login fix and the logout path

packages/server/src/services/registry.ts already shows awareness of the problem. A September 2025 change (“fix: use shellsafe docker command”) introduced an shEscape() function and a safeDockerLoginCommand() helper that quotes the registry, the username and the password before building the docker login command.

We checked every command built from a registry record. Further down in the same file, removeRegistry() builds its docker logout command by plain string interpolation. The value it interpolates, registryUrl, is validated as z.string() in the Zod schema, with no restriction on characters.

Root cause

The safe and the unsafe constructions sit in the same file:

// packages/server/src/services/registry.ts (0.29.2)
function shEscape(s: string | undefined): string {
	if (!s) return "''";
	return `'${s.replace(/'/g, `'\\''`)}'`;
}

export function safeDockerLoginCommand(registry, user, pass) {
	const escapedRegistry = shEscape(registry);
	const escapedUser = shEscape(user);
	const escapedPassword = shEscape(pass);
	return `printf %s ${escapedPassword} | docker login ${escapedRegistry} -u ${escapedUser} --password-stdin`;
}

export const removeRegistry = async (registryId: string) => {
	// ... delete the row, keep it in `response`
	if (!IS_CLOUD) {
		await execAsync(`docker logout ${response.registryUrl}`);
	}
	return response;
};

shEscape() wraps a value in single quotes and rewrites any embedded quote as '\'', which is the correct way to pass an arbitrary string as a single word to a POSIX shell. removeRegistry() skips it. The stored URL becomes part of a shell program, so a semicolon ends docker logout and starts a new command (CWE-78). The IS_CLOUD guard means the code path only exists on self-hosted instances, which are the ones that run commands on their own host.

Exploitation

The chain uses two tRPC operations: store a registry URL containing shell metacharacters, then delete that registry. The reliable way to retain the value is registry.update on an existing record: registry.create may roll back its insertion if the initial docker login fails. The account needs permission to update and delete registries.

On deletion, the server runs:

docker logout ; id > /tmp/pwned #

The trailing # comments out anything that might follow. In the 0.29.2 code, createRegistry() inserts the row and runs docker login inside one database transaction. updateRegistry() writes the new values before attempting login, outside that transaction. Deletion needs only the stored value; the vulnerable docker logout does not depend on a reachable registry. Our initial report suggested a direct creation without accounting for the possible transaction rollback, so that sequence alone is not a reliable proof.

Impact

The GitHub advisory scores the flaw 8.8 (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H). The attacker needs an account in the organisation whose role grants the registry permissions: in the 0.29.2 router, registry.create and registry.update require withPermission("registry", "create") and registry.remove requires withPermission("registry", "delete"). Managing registries is an operational task that platforms routinely hand to developers, which fits PR:L.

What the attacker gets is command execution with the privileges of the Dokploy server process, on the main host. That process runs Docker commands for the whole platform, so the injected command can start containers, read their environment and mount host paths. It also has access to the platform’s database, with the secrets of every project, and to the SSH keys used to reach remote servers. A permission meant to manage image credentials becomes control over the deployment platform and, through it, over every application it hosts.

The fix

The fix is commit 547ba2d, dated 2026-05-09, shipped in Dokploy 0.29.3 on 2026-05-11. The advisory itself lists the version range as <= 0.0.0 with no patched version, which is malformed; the first release tag containing the commit is v0.29.3, and the docker logout line in v0.29.2 is still unescaped.

The commit addresses both ends of the flow. The sink applies the existing escape:

// packages/server/src/services/registry.ts (0.29.3)
await execAsync(`docker logout ${shEscape(response.registryUrl)}`);

The entry point gains a strict format for registry URLs, used by the create, test and update schemas (apiUpdateRegistry derives from the same createSchema):

// packages/server/src/db/schema/registry.ts (0.29.3)
const registryUrlSchema = z
	.string()
	.refine(
		(val) =>
			val === "" ||
			/^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?(:\d{1,5})?$/.test(val),
		"Registry URL must be a valid hostname or hostname:port (e.g. registry.example.com or localhost:5000)",
	);

The two layers are complementary. The validation stops new malicious values, and the escape at the sink also neutralises a value that was stored before the upgrade, which validation alone would miss.

The same pattern survived elsewhere for a few more weeks. Commit 1f4f940 (#4579), released in 0.29.8 on 2026-06-08, replaced two more hand-built login commands, in utils/cluster/upload.ts and utils/providers/docker.ts, that interpolated the registry password inside double quotes (where $(...) is still expanded) and the username inside single quotes. Both now go through safeDockerLoginCommand().

Detection and mitigation

  1. Upgrade Dokploy to 0.29.3 or later, preferably 0.29.8 or later, which also routes the remaining docker login constructions through the escaping helper.
  2. Review the registries stored in the Dokploy database and look for any registryUrl that is not a plain hostname or hostname:port; a value containing ;, |, &, $, backticks, quotes, spaces or # indicates an attempt.
  3. Review which roles hold the registry create and delete permissions, and restrict them to the people who manage image credentials.
  4. Use the Dokploy audit log, which records registry creation, update and deletion, to find registries created and deleted in quick succession or by unexpected accounts.
  5. On the host, watch for processes whose parent is the sh -c docker logout ... spawned by the Dokploy Node.js process, and more generally for shells, curl, wget or network tools started by that process outside deployments.

Timeline

Date Event
2026-03-29 Private report to the maintainers through a GitHub security advisory
2026-05-09 Escape at the sink and URL validation committed (547ba2d)
2026-05-11 Dokploy 0.29.3 released, advisory GHSA-827c-7x62-29jq published with CVE-2026-45662
2026-06-08 Dokploy 0.29.8 released with the remaining login commands escaped (1f4f940, #4579)

References

  1. Vendor advisory · CVE-2026-45662 github.com/Dokploy/dokploy/security/advisories/GHSA-827c-7x62-29jq
  2. NVD entry · CVE-2026-45662 nvd.nist.gov/vuln/detail/CVE-2026-45662
  3. CVE.org record · CVE-2026-45662 www.cve.org/CVERecord?id=CVE-2026-45662
  4. MITRE CWE-78 cwe.mitre.org/data/definitions/78.html
  5. Project repository github.com/Dokploy/dokploy

Cite this advisory

rootzero. “CVE-2026-45662: command injection in Dokploy through the registry URL on docker logout”. Vulnerability research, rootzero, 11 May 2026. https://rootzero.eu/en/research/cve/cve-2026-45662-dokploy-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.