From 9d5987f514b5a0b843c8b7bbfb0c0c038d345c09 Mon Sep 17 00:00:00 2001 From: ETdoFresh Date: Mon, 13 Jul 2026 10:19:57 -0500 Subject: [PATCH] fix(env): reject undecrypted environment ciphertext --- apps/dokploy/__test__/env/shared.test.ts | 15 ++++++++++++++ packages/server/src/utils/docker/utils.ts | 25 ++++++++++++++++++++--- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/apps/dokploy/__test__/env/shared.test.ts b/apps/dokploy/__test__/env/shared.test.ts index 5e231a5ccf..50cb91899b 100644 --- a/apps/dokploy/__test__/env/shared.test.ts +++ b/apps/dokploy/__test__/env/shared.test.ts @@ -1,4 +1,5 @@ import { prepareEnvironmentVariables } from "@dokploy/server/index"; +import { encryptValue } from "@dokploy/server/lib/encryption"; import { describe, expect, it } from "vitest"; const projectEnv = ` @@ -13,6 +14,20 @@ SERVICE_PORT=4000 `; describe("prepareEnvironmentVariables", () => { + it("rejects encrypted values that were not decrypted by the database layer", () => { + const encrypted = encryptValue("SECRET=value"); + + expect(() => prepareEnvironmentVariables(encrypted, "", "")).toThrow( + "Unable to decrypt environment variables for the service scope", + ); + expect(() => prepareEnvironmentVariables("", encrypted, "")).toThrow( + "Unable to decrypt environment variables for the project scope", + ); + expect(() => prepareEnvironmentVariables("", "", encrypted)).toThrow( + "Unable to decrypt environment variables for the environment scope", + ); + }); + it("resolves project variables correctly", () => { const resolved = prepareEnvironmentVariables(serviceEnv, projectEnv); diff --git a/packages/server/src/utils/docker/utils.ts b/packages/server/src/utils/docker/utils.ts index 8065b7dd93..44a3ea7b02 100644 --- a/packages/server/src/utils/docker/utils.ts +++ b/packages/server/src/utils/docker/utils.ts @@ -2,6 +2,7 @@ import fs from "node:fs"; import path from "node:path"; import type { Readable } from "node:stream"; import { docker, paths } from "@dokploy/server/constants"; +import { isEncrypted } from "@dokploy/server/lib/encryption"; import type { Compose } from "@dokploy/server/services/compose"; import type { ContainerInfo, ResourceRequirements } from "dockerode"; import { parse } from "dotenv"; @@ -396,14 +397,32 @@ export const removeService = async ( } }; +// encryptedText intentionally returns raw ciphertext when decryption fails. +// Never let dotenv interpret that ciphertext as an empty environment block. +const parseDecryptedEnvironmentVariables = ( + value: string | null | undefined, + scope: "service" | "project" | "environment", +) => { + const input = value ?? ""; + if (isEncrypted(input)) { + throw new Error( + `Unable to decrypt environment variables for the ${scope} scope. Ensure ENCRYPTION_KEY or BETTER_AUTH_SECRET is consistent across all Dokploy services.`, + ); + } + return parse(input); +}; + export const prepareEnvironmentVariables = ( serviceEnv: string | null, projectEnv?: string | null, environmentEnv?: string | null, ) => { - const projectVars = parse(projectEnv ?? ""); - const environmentVars = parse(environmentEnv ?? ""); - const serviceVars = parse(serviceEnv ?? ""); + const projectVars = parseDecryptedEnvironmentVariables(projectEnv, "project"); + const environmentVars = parseDecryptedEnvironmentVariables( + environmentEnv, + "environment", + ); + const serviceVars = parseDecryptedEnvironmentVariables(serviceEnv, "service"); const resolvedVars = Object.entries(serviceVars).map(([key, value]) => { let resolvedValue = value;