diff --git a/apps/dokploy/__test__/env/environment.test.ts b/apps/dokploy/__test__/env/environment.test.ts index 24ef18b009..6f7d71f5c1 100644 --- a/apps/dokploy/__test__/env/environment.test.ts +++ b/apps/dokploy/__test__/env/environment.test.ts @@ -1,6 +1,7 @@ import { prepareEnvironmentVariables, prepareEnvironmentVariablesForShell, + quoteDotenvValue, } from "@dokploy/server/index"; import { describe, expect, it } from "vitest"; @@ -363,6 +364,50 @@ SIMPLE=\${{environment.SIMPLE_VAR}} }); }); +describe("quoteDotenvValue (dotenv value quoting)", () => { + it("wraps a simple value in double quotes", () => { + expect(quoteDotenvValue("KEY=value")).toBe('KEY="value"'); + }); + + it("returns pair unchanged when no = present", () => { + expect(quoteDotenvValue("NOEQUALS")).toBe("NOEQUALS"); + }); + + it("handles empty value", () => { + expect(quoteDotenvValue("KEY=")).toBe('KEY=""'); + }); + + it("escapes backslashes", () => { + expect(quoteDotenvValue("PATH=C:\\Users\\docs")).toBe( + 'PATH="C:\\\\Users\\\\docs"', + ); + }); + + it("escapes double quotes in values", () => { + expect(quoteDotenvValue('MSG=say "hello"')).toBe( + 'MSG="say \\"hello\\""', + ); + }); + + it("escapes literal newlines from dotenv expansion", () => { + expect(quoteDotenvValue("MSG=line1\nline2")).toBe('MSG="line1\\nline2"'); + }); + + it("escapes carriage returns", () => { + expect(quoteDotenvValue("MSG=line1\rline2")).toBe('MSG="line1\\rline2"'); + }); + + it("escapes dollar signs to prevent variable expansion", () => { + expect(quoteDotenvValue("PRICE=$100")).toBe('PRICE="\\$100"'); + }); + + it("handles value with multiple special characters", () => { + expect(quoteDotenvValue('COMPLEX=a\\b"c\n$d')).toBe( + 'COMPLEX="a\\\\b\\"c\\n\\$d"', + ); + }); +}); + describe("prepareEnvironmentVariablesForShell (shell escaping)", () => { it("escapes single quotes in environment variable values", () => { const serviceEnv = ` diff --git a/packages/server/src/utils/builders/compose.ts b/packages/server/src/utils/builders/compose.ts index 3165706266..fb881af3f7 100644 --- a/packages/server/src/utils/builders/compose.ts +++ b/packages/server/src/utils/builders/compose.ts @@ -8,6 +8,7 @@ import { encodeBase64, getEnvironmentVariablesObject, prepareEnvironmentVariables, + quoteDotenvValue, } from "../docker/utils"; export type ComposeNested = InferResultType< @@ -119,7 +120,9 @@ export const getCreateEnvFileCommand = (compose: ComposeNested) => { envContent, compose.environment.project.env, compose.environment.env, - ).join("\n"); + ) + .map(quoteDotenvValue) + .join("\n"); const encodedContent = encodeBase64(envFileContent); return ` diff --git a/packages/server/src/utils/builders/utils.ts b/packages/server/src/utils/builders/utils.ts index 97ea69ef8c..894d076900 100644 --- a/packages/server/src/utils/builders/utils.ts +++ b/packages/server/src/utils/builders/utils.ts @@ -1,5 +1,9 @@ import { dirname, join } from "node:path"; -import { encodeBase64, prepareEnvironmentVariables } from "../docker/utils"; +import { + encodeBase64, + prepareEnvironmentVariables, + quoteDotenvValue, +} from "../docker/utils"; export const createEnvFileCommand = ( directory: string, @@ -11,7 +15,9 @@ export const createEnvFileCommand = ( env, projectEnv, environmentEnv, - ).join("\n"); + ) + .map(quoteDotenvValue) + .join("\n"); const encodedContent = encodeBase64(envFileContent || ""); const envFilePath = join(dirname(directory), ".env"); diff --git a/packages/server/src/utils/docker/utils.ts b/packages/server/src/utils/docker/utils.ts index dd645cd1bf..e6f7bb5c23 100644 --- a/packages/server/src/utils/docker/utils.ts +++ b/packages/server/src/utils/docker/utils.ts @@ -408,6 +408,24 @@ export const prepareEnvironmentVariables = ( return resolvedVars; }; +const DOTENV_ESCAPE_MAP: Record = { + "\\": "\\\\", + '"': '\\"', + "\n": "\\n", + "\r": "\\r", + $: "\\$", +}; + +export const quoteDotenvValue = (pair: string): string => { + const eqIndex = pair.indexOf("="); + if (eqIndex === -1) return pair; + const key = pair.substring(0, eqIndex); + const value = pair + .substring(eqIndex + 1) + .replace(/[\\"$\n\r]/g, (ch) => DOTENV_ESCAPE_MAP[ch] ?? ch); + return `${key}="${value}"`; +}; + export const prepareEnvironmentVariablesForShell = ( serviceEnv: string | null, projectEnv?: string | null,