From d3f522b7a6f5100fc0fc0bff5851e48d11d459e2 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Mon, 20 Jul 2026 17:17:45 -0600 Subject: [PATCH] fix(security): command injection in registry.testRegistry/testRegistryById remote path The remote (execAsyncRemote) path built `echo ${password} | docker ${args.join(" ")}` with the password, registryUrl and username interpolated unescaped, so a password like `pw; whoami` ran arbitrary commands as root on the target server. Reuse safeDockerLoginCommand (already used by create/update), which shell-escapes each field and feeds the password via --password-stdin. The local argv+stdin path was already safe. --- apps/dokploy/server/api/routers/registry.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/apps/dokploy/server/api/routers/registry.ts b/apps/dokploy/server/api/routers/registry.ts index 1b156c069..c93da5a22 100644 --- a/apps/dokploy/server/api/routers/registry.ts +++ b/apps/dokploy/server/api/routers/registry.ts @@ -5,6 +5,7 @@ import { findRegistryById, IS_CLOUD, removeRegistry, + safeDockerLoginCommand, updateRegistry, } from "@dokploy/server"; import { db } from "@dokploy/server/db"; @@ -122,7 +123,11 @@ export const registryRouter = createTRPCRouter({ if (input.serverId && input.serverId !== "none") { await execAsyncRemote( input.serverId, - `echo ${input.password} | docker ${args.join(" ")}`, + safeDockerLoginCommand( + input.registryUrl, + input.username, + input.password, + ), ); } else { await execFileAsync("docker", args, { @@ -182,7 +187,11 @@ export const registryRouter = createTRPCRouter({ if (input.serverId && input.serverId !== "none") { await execAsyncRemote( input.serverId, - `echo ${registryData.password} | docker ${args.join(" ")}`, + safeDockerLoginCommand( + registryData.registryUrl, + registryData.username, + registryData.password, + ), ); } else { await execFileAsync("docker", args, {