From c14cbcec71df547335e4dd9b45b3016ceb4e396f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 23:01:38 +0000 Subject: [PATCH] fix: close remaining backup credential and FTPS bypasses --- apps/dokploy/__test__/utils/backups.test.ts | 71 ++++++++++++++----- .../server/src/db/validations/destination.ts | 34 +++++++-- .../server/src/utils/restore/web-server.ts | 14 ++-- 3 files changed, 87 insertions(+), 32 deletions(-) diff --git a/apps/dokploy/__test__/utils/backups.test.ts b/apps/dokploy/__test__/utils/backups.test.ts index d70f1415b..e9ad6b6d2 100644 --- a/apps/dokploy/__test__/utils/backups.test.ts +++ b/apps/dokploy/__test__/utils/backups.test.ts @@ -332,7 +332,18 @@ describe("FTP TLS certificate verification", () => { endpoint: "storage.example.com", }; - test.each(["--ftp-no-check-certificate", "--no-check-certificate"])( + test.each([ + "--ftp-no-check-certificate", + "--ftp-no-check-certificate=true", + "--ftp-no-check-certificate=TRUE", + "--ftp-no-check-certificate=1", + "--ftp-no-check-certificate=t", + "--no-check-certificate", + "--no-check-certificate=true", + "--no-check-certificate=TRUE", + "--no-check-certificate=1", + "--no-check-certificate=t", + ])( "rejects certificate-verification bypass %s at schema validation", (flag) => { expect( @@ -344,28 +355,38 @@ describe("FTP TLS certificate verification", () => { }, ); - test.each(["--ftp-no-check-certificate", "--no-check-certificate"])( - "rejects certificate-verification bypass %s at runtime", - async (flag) => { - await expect( - getRclonePathAndFlags( - destination({ - provider: RCLONE_DESTINATION_PROVIDERS.FTP, - endpoint: "storage.example.com", - accessKey: "backup-user", - secretAccessKey: "", - region: "", - bucket: "backups", - additionalFlags: ["--ftp-explicit-tls", flag], - }), - ), - ).rejects.toThrow("FTP TLS certificate verification cannot be disabled"); - }, - ); + test.each([ + "--ftp-no-check-certificate", + "--ftp-no-check-certificate=TRUE", + "--ftp-no-check-certificate=1", + "--no-check-certificate", + "--no-check-certificate=TRUE", + "--no-check-certificate=1", + ])("rejects certificate-verification bypass %s at runtime", async (flag) => { + await expect( + getRclonePathAndFlags( + destination({ + provider: RCLONE_DESTINATION_PROVIDERS.FTP, + endpoint: "storage.example.com", + accessKey: "backup-user", + secretAccessKey: "", + region: "", + bucket: "backups", + additionalFlags: ["--ftp-explicit-tls", flag], + }), + ), + ).rejects.toThrow("FTP TLS certificate verification cannot be disabled"); + }); test.each([ "--ftp-no-check-certificate=false", + "--ftp-no-check-certificate=FALSE", + "--ftp-no-check-certificate=0", + "--ftp-no-check-certificate=f", "--no-check-certificate=false", + "--no-check-certificate=FALSE", + "--no-check-certificate=0", + "--no-check-certificate=f", ])("allows explicitly safe certificate flag %s", (flag) => { expect( apiCreateDestination.safeParse({ @@ -374,4 +395,16 @@ describe("FTP TLS certificate verification", () => { }).success, ).toBe(true); }); + test.each([ + "--ftp-explicit-tls=TRUE", + "--ftp-explicit-tls=1", + "--ftp-explicit-tls=t", + ])("accepts pflag-compatible TLS true value %s", (flag) => { + expect( + apiCreateDestination.safeParse({ + ...input, + additionalFlags: [flag], + }).success, + ).toBe(true); + }); }); diff --git a/packages/server/src/db/validations/destination.ts b/packages/server/src/db/validations/destination.ts index ad7309e04..39b2c32de 100644 --- a/packages/server/src/db/validations/destination.ts +++ b/packages/server/src/db/validations/destination.ts @@ -42,12 +42,32 @@ export const FTP_CERTIFICATE_VERIFICATION_REQUIRED_ERROR = export const SFTP_HOST_KEY_REQUIRED_ERROR = "SFTP destinations must verify the server host key. Add --sftp-known-hosts-file=/path/to/known_hosts."; +const parseBooleanFlagValue = ( + flag: string, + flagName: string, +): boolean | undefined => { + if (flag === flagName) return true; + const prefix = `${flagName}=`; + if (!flag.startsWith(prefix)) return undefined; + + const value = flag.slice(prefix.length).toLowerCase(); + if (["1", "t", "true"].includes(value)) return true; + if (["0", "f", "false"].includes(value)) return false; + return undefined; +}; + +const getBooleanFlagValues = (flags: readonly string[], flagName: string) => + flags + .filter((flag) => flag === flagName || flag.startsWith(`${flagName}=`)) + .map((flag) => parseBooleanFlagValue(flag, flagName)); + const isBooleanFlagEnabled = ( flags: readonly string[], flagName: string, -): boolean => - (flags.includes(flagName) || flags.includes(`${flagName}=true`)) && - !flags.includes(`${flagName}=false`); +): boolean => { + const values = getBooleanFlagValues(flags, flagName); + return values.length > 0 && values.every((value) => value === true); +}; export const getFtpTlsState = (flags: readonly string[] | null | undefined) => { const values = flags ?? []; @@ -62,7 +82,13 @@ export const hasDisabledFtpCertificateVerification = ( ): boolean => { const values = flags ?? []; return ["--ftp-no-check-certificate", "--no-check-certificate"].some( - (flag) => values.includes(flag) || values.includes(`${flag}=true`), + (flagName) => { + const matchingValues = getBooleanFlagValues(values, flagName); + return ( + matchingValues.length > 0 && + matchingValues.some((value) => value !== false) + ); + }, ); }; diff --git a/packages/server/src/utils/restore/web-server.ts b/packages/server/src/utils/restore/web-server.ts index 9b7d7e1d1..cddd37b8b 100644 --- a/packages/server/src/utils/restore/web-server.ts +++ b/packages/server/src/utils/restore/web-server.ts @@ -4,6 +4,7 @@ import { join } from "node:path"; import { IS_CLOUD, paths } from "@dokploy/server/constants"; import type { Destination } from "@dokploy/server/services/destination"; import { quote } from "shell-quote"; +import { getSafeRcloneErrorMessage } from "../backups/redact"; import { getRclonePathAndFlags } from "../backups/utils"; import { execAsync } from "../process/execAsync"; @@ -146,14 +147,9 @@ export const restoreWebServerBackup = async ( await execAsync(`rm -rf ${quote([tempDir])}`); } } catch (error) { - console.error(error); - emit( - `Error: ${ - error instanceof Error - ? error.message - : "Error restoring web server backup" - }`, - ); - throw error; + const safeErrorMessage = getSafeRcloneErrorMessage(error); + console.error("Restore error:", safeErrorMessage); + emit(`Error: ${safeErrorMessage}`); + throw new Error(safeErrorMessage); } };