diff --git a/packages/server/src/utils/builders/compose.ts b/packages/server/src/utils/builders/compose.ts index 1694a48ed..89851d84c 100644 --- a/packages/server/src/utils/builders/compose.ts +++ b/packages/server/src/utils/builders/compose.ts @@ -70,7 +70,8 @@ Compose Type: ${composeType} ✅`; // Shell control characters that must never appear in a user-provided compose // command: they would let it break out of the `docker ${command}` invocation // into arbitrary host commands. A normal docker compose CLI line never needs them. -const UNSAFE_COMPOSE_COMMAND = /[;&|`$(){}<>\n\\]/; +// Removed '&' from the blocklist to allow '&&' chaining +const UNSAFE_COMPOSE_COMMAND = /[;|`$(){}<>\n\\]/; const sanitizeCommand = (command: string) => { const sanitizedCommand = command.trim(); @@ -81,8 +82,24 @@ const sanitizeCommand = (command: string) => { ); } - const parts = sanitizedCommand.split(/\s+/); + if (sanitizedCommand.includes("&")) { + // Block single '&' (e.g., backgrounding tasks) or malformed chains like '&&&' + if (/(? cmd.trim()); + const isSafeChain = chains.slice(1).every((cmd) => + cmd.startsWith("docker compose ") || cmd.startsWith("docker-compose ") + ); + + if (!isSafeChain) { + throw new Error("Chained commands must strictly start with 'docker compose '"); + } + } + + const parts = sanitizedCommand.split(/\s+/); const restCommand = parts.map((arg) => arg.replace(/^"(.*)"$/, "$1")); return restCommand.join(" ");