diff --git a/apps/dokploy/server/api/routers/backup.ts b/apps/dokploy/server/api/routers/backup.ts index c324f5cc1..9bc73ce77 100644 --- a/apps/dokploy/server/api/routers/backup.ts +++ b/apps/dokploy/server/api/routers/backup.ts @@ -49,6 +49,7 @@ import { restoreWebServerBackup, } from "@dokploy/server/utils/restore"; import { TRPCError } from "@trpc/server"; +import { quote } from "shell-quote"; import { z } from "zod"; import { createTRPCRouter, @@ -510,7 +511,7 @@ export const backupRouter = createTRPCRouter({ : input.search; const searchPath = baseDir ? `${bucketPath}/${baseDir}` : bucketPath; - const listCommand = `rclone lsjson ${rcloneFlags.join(" ")} "${searchPath}" --no-mimetype --no-modtime 2>/dev/null`; + const listCommand = `rclone lsjson ${rcloneFlags.join(" ")} ${quote([searchPath])} --no-mimetype --no-modtime 2>/dev/null`; let stdout = ""; diff --git a/apps/dokploy/server/api/routers/destination.ts b/apps/dokploy/server/api/routers/destination.ts index cf7395a3f..3d2ad6057 100644 --- a/apps/dokploy/server/api/routers/destination.ts +++ b/apps/dokploy/server/api/routers/destination.ts @@ -10,6 +10,7 @@ import { import { db } from "@dokploy/server/db"; import { TRPCError } from "@trpc/server"; import { desc, eq } from "drizzle-orm"; +import { quote } from "shell-quote"; import { createTRPCRouter, withPermission } from "@/server/api/trpc"; import { audit } from "@/server/api/utils/audit"; import { @@ -58,10 +59,10 @@ export const destinationRouter = createTRPCRouter({ } = input; try { const rcloneFlags = [ - `--s3-access-key-id="${accessKey}"`, - `--s3-secret-access-key="${secretAccessKey}"`, - `--s3-region="${region}"`, - `--s3-endpoint="${endpoint}"`, + `--s3-access-key-id=${quote([accessKey])}`, + `--s3-secret-access-key=${quote([secretAccessKey])}`, + `--s3-region=${quote([region])}`, + `--s3-endpoint=${quote([endpoint])}`, "--s3-no-check-bucket", "--s3-force-path-style", "--retries 1", @@ -70,13 +71,13 @@ export const destinationRouter = createTRPCRouter({ "--contimeout 5s", ]; if (provider) { - rcloneFlags.unshift(`--s3-provider="${provider}"`); + rcloneFlags.unshift(`--s3-provider=${quote([provider])}`); } if (additionalFlags?.length) { rcloneFlags.push(...additionalFlags); } const rcloneDestination = `:s3:${bucket}`; - const rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}"`; + const rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} ${quote([rcloneDestination])}`; if (IS_CLOUD && !input.serverId) { throw new TRPCError({ diff --git a/apps/dokploy/server/api/routers/volume-backups.ts b/apps/dokploy/server/api/routers/volume-backups.ts index 4781485fc..433b371eb 100644 --- a/apps/dokploy/server/api/routers/volume-backups.ts +++ b/apps/dokploy/server/api/routers/volume-backups.ts @@ -13,6 +13,8 @@ import { db } from "@dokploy/server/db"; import { createVolumeBackupSchema, updateVolumeBackupSchema, + VOLUME_NAME_MESSAGE, + VOLUME_NAME_REGEX, volumeBackups, } from "@dokploy/server/db/schema"; import { findDestinationById } from "@dokploy/server/services/destination"; @@ -275,7 +277,10 @@ export const volumeBackupsRouter = createTRPCRouter({ z.object({ backupFileName: z.string().min(1), destinationId: z.string().min(1), - volumeName: z.string().min(1), + volumeName: z + .string() + .min(1) + .regex(VOLUME_NAME_REGEX, VOLUME_NAME_MESSAGE), id: z.string().min(1), serviceType: z.enum(["application", "compose"]), serverId: z.string().optional(), diff --git a/packages/server/src/db/schema/utils.ts b/packages/server/src/db/schema/utils.ts index 4ba9007e1..4bf140738 100644 --- a/packages/server/src/db/schema/utils.ts +++ b/packages/server/src/db/schema/utils.ts @@ -41,6 +41,12 @@ export const APP_NAME_REGEX = /^[a-zA-Z0-9._-]+$/; export const APP_NAME_MESSAGE = "App name can only contain letters, numbers, dots, underscores and hyphens"; +/** Docker volume name: must start alphanumeric, then letters/numbers/._- only. Safe for shell. */ +export const VOLUME_NAME_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/; + +export const VOLUME_NAME_MESSAGE = + "Volume name must start with a letter or number and contain only letters, numbers, dots, underscores and hyphens"; + /** Database password: blocks shell-dangerous characters like $ ! ' " \ / and spaces. */ export const DATABASE_PASSWORD_REGEX = /^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/; diff --git a/packages/server/src/db/schema/volume-backups.ts b/packages/server/src/db/schema/volume-backups.ts index fd3b48253..bf025126d 100644 --- a/packages/server/src/db/schema/volume-backups.ts +++ b/packages/server/src/db/schema/volume-backups.ts @@ -14,7 +14,11 @@ import { serviceType } from "./mount"; import { mysql } from "./mysql"; import { postgres } from "./postgres"; import { redis } from "./redis"; -import { generateAppName } from "./utils"; +import { + generateAppName, + VOLUME_NAME_MESSAGE, + VOLUME_NAME_REGEX, +} from "./utils"; export const volumeBackups = pgTable("volume_backup", { volumeBackupId: text("volumeBackupId") @@ -113,7 +117,9 @@ export const volumeBackupsRelations = relations( }), ); -export const createVolumeBackupSchema = createInsertSchema(volumeBackups).omit({ +export const createVolumeBackupSchema = createInsertSchema(volumeBackups, { + volumeName: z.string().regex(VOLUME_NAME_REGEX, VOLUME_NAME_MESSAGE), +}).omit({ volumeBackupId: true, }); diff --git a/packages/server/src/services/certificate.ts b/packages/server/src/services/certificate.ts index aa5c3983c..1c6137506 100644 --- a/packages/server/src/services/certificate.ts +++ b/packages/server/src/services/certificate.ts @@ -9,6 +9,7 @@ import { import { removeDirectoryIfExistsContent } from "@dokploy/server/utils/filesystem/directory"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; +import { quote } from "shell-quote"; import { stringify } from "yaml"; import type { z } from "zod"; import { encodeBase64 } from "../utils/docker/utils"; @@ -63,7 +64,7 @@ export const removeCertificateById = async (certificateId: string) => { const certDir = path.join(CERTIFICATES_PATH, certificate.certificatePath); if (certificate.serverId) { - await execAsyncRemote(certificate.serverId, `rm -rf ${certDir}`); + await execAsyncRemote(certificate.serverId, `rm -rf ${quote([certDir])}`); } else { await removeDirectoryIfExistsContent(certDir); } @@ -108,10 +109,10 @@ const createCertificateFiles = async (certificate: Certificate) => { const certificateData = encodeBase64(certificate.certificateData); const privateKey = encodeBase64(certificate.privateKey); const command = ` - mkdir -p ${certDir}; - echo "${certificateData}" | base64 -d > "${crtPath}"; - echo "${privateKey}" | base64 -d > "${keyPath}"; - echo "${yamlConfig}" > "${configFile}"; + mkdir -p ${quote([certDir])}; + echo "${certificateData}" | base64 -d > ${quote([crtPath])}; + echo "${privateKey}" | base64 -d > ${quote([keyPath])}; + echo "${yamlConfig}" > ${quote([configFile])}; `; await execAsyncRemote(certificate.serverId, command); diff --git a/packages/server/src/services/mount.ts b/packages/server/src/services/mount.ts index 1987ded27..e55075ad3 100644 --- a/packages/server/src/services/mount.ts +++ b/packages/server/src/services/mount.ts @@ -18,6 +18,7 @@ import { } from "@dokploy/server/utils/process/execAsync"; import { TRPCError } from "@trpc/server"; import { eq, type SQL, sql } from "drizzle-orm"; +import { quote } from "shell-quote"; import type { z } from "zod"; export type Mount = typeof mounts.$inferSelect; @@ -317,7 +318,7 @@ export const updateFileMount = async (mountId: string) => { try { const serverId = await getServerId(mount); const encodedContent = encodeBase64(mount.content || ""); - const command = `echo "${encodedContent}" | base64 -d > ${fullPath}`; + const command = `echo "${encodedContent}" | base64 -d > ${quote([fullPath])}`; if (serverId) { await execAsyncRemote(serverId, command); } else { @@ -337,7 +338,7 @@ export const deleteFileMount = async (mountId: string) => { try { const serverId = await getServerId(mount); if (serverId) { - const command = `rm -rf ${fullPath}`; + const command = `rm -rf ${quote([fullPath])}`; await execAsyncRemote(serverId, command); } else { await removeFileOrDirectory(fullPath); diff --git a/packages/server/src/services/patch-repo.ts b/packages/server/src/services/patch-repo.ts index 35e734533..f946d1dcd 100644 --- a/packages/server/src/services/patch-repo.ts +++ b/packages/server/src/services/patch-repo.ts @@ -1,6 +1,7 @@ import { join } from "node:path"; import { paths } from "@dokploy/server/constants"; import { TRPCError } from "@trpc/server"; +import { quote } from "shell-quote"; import { execAsync, execAsyncRemote } from "../utils/process/execAsync"; import { cloneBitbucketRepository } from "../utils/providers/bitbucket"; import { cloneGitRepository } from "../utils/providers/git"; @@ -85,7 +86,7 @@ export const readPatchRepoDirectory = async ( serverId?: string | null, ): Promise => { // Use git ls-tree to get tracked files only - const command = `cd "${repoPath}" && git ls-tree -r --name-only HEAD`; + const command = `cd ${quote([repoPath])} && git ls-tree -r --name-only HEAD`; let stdout: string; try { @@ -168,7 +169,7 @@ export const readPatchRepoFile = async ( const repoPath = join(PATCH_REPOS_PATH, type, application.appName); const fullPath = join(repoPath, filePath); - const command = `cat "${fullPath}"`; + const command = `cat ${quote([fullPath])}`; if (serverId) { const result = await execAsyncRemote(serverId, command); diff --git a/packages/server/src/utils/backups/utils.ts b/packages/server/src/utils/backups/utils.ts index ed22067f8..dba843956 100644 --- a/packages/server/src/utils/backups/utils.ts +++ b/packages/server/src/utils/backups/utils.ts @@ -72,16 +72,16 @@ export const getS3Credentials = (destination: Destination) => { const { accessKey, secretAccessKey, region, endpoint, provider } = destination; const rcloneFlags = [ - `--s3-access-key-id="${accessKey}"`, - `--s3-secret-access-key="${secretAccessKey}"`, - `--s3-region="${region}"`, - `--s3-endpoint="${endpoint}"`, + `--s3-access-key-id=${quote([accessKey])}`, + `--s3-secret-access-key=${quote([secretAccessKey])}`, + `--s3-region=${quote([region])}`, + `--s3-endpoint=${quote([endpoint])}`, "--s3-no-check-bucket", "--s3-force-path-style", ]; if (provider) { - rcloneFlags.unshift(`--s3-provider="${provider}"`); + rcloneFlags.unshift(`--s3-provider=${quote([provider])}`); } if (destination.additionalFlags?.length) { diff --git a/packages/server/src/utils/databases/rebuild.ts b/packages/server/src/utils/databases/rebuild.ts index 0d1517d3a..291c10746 100644 --- a/packages/server/src/utils/databases/rebuild.ts +++ b/packages/server/src/utils/databases/rebuild.ts @@ -14,6 +14,7 @@ import { deployMySql } from "@dokploy/server/services/mysql"; import { deployPostgres } from "@dokploy/server/services/postgres"; import { deployRedis } from "@dokploy/server/services/redis"; import { eq } from "drizzle-orm"; +import { quote } from "shell-quote"; import { removeService } from "../docker/utils"; import { execAsync, execAsyncRemote } from "../process/execAsync"; @@ -40,7 +41,7 @@ export const rebuildDatabase = async ( for (const mount of database.mounts) { if (mount.type === "volume") { - const command = `docker volume rm ${mount?.volumeName} --force`; + const command = `docker volume rm ${quote([mount?.volumeName ?? ""])} --force`; if (database.serverId) { await execAsyncRemote(database.serverId, command); } else { diff --git a/packages/server/src/utils/docker/utils.ts b/packages/server/src/utils/docker/utils.ts index 8065b7dd9..1aecb1d75 100644 --- a/packages/server/src/utils/docker/utils.ts +++ b/packages/server/src/utils/docker/utils.ts @@ -712,14 +712,14 @@ export const getCreateFileCommand = ( ) => { const fullPath = path.join(outputPath, filePath); if (fullPath.endsWith(path.sep) || filePath.endsWith("/")) { - return `mkdir -p ${fullPath};`; + return `mkdir -p ${quote([fullPath])};`; } const directory = path.dirname(fullPath); const encodedContent = encodeBase64(content); return ` - mkdir -p ${directory}; - echo "${encodedContent}" | base64 -d > "${fullPath}"; + mkdir -p ${quote([directory])}; + echo "${encodedContent}" | base64 -d > ${quote([fullPath])}; `; }; diff --git a/packages/server/src/utils/restore/compose.ts b/packages/server/src/utils/restore/compose.ts index 946c94a38..07f23c772 100644 --- a/packages/server/src/utils/restore/compose.ts +++ b/packages/server/src/utils/restore/compose.ts @@ -1,6 +1,7 @@ import type { apiRestoreBackup } from "@dokploy/server/db/schema"; import type { Compose } from "@dokploy/server/services/compose"; import type { Destination } from "@dokploy/server/services/destination"; +import { quote } from "shell-quote"; import type { z } from "zod"; import { getS3Credentials } from "../backups/utils"; import { execAsync, execAsyncRemote } from "../process/execAsync"; @@ -26,10 +27,10 @@ export const restoreComposeBackup = async ( const rcloneFlags = getS3Credentials(destination); const bucketPath = `:s3:${destination.bucket}`; const backupPath = `${bucketPath}/${backupInput.backupFile}`; - let rcloneCommand = `rclone cat ${rcloneFlags.join(" ")} "${backupPath}" | gunzip`; + let rcloneCommand = `rclone cat ${rcloneFlags.join(" ")} ${quote([backupPath])} | gunzip`; if (backupInput.metadata?.mongo) { - rcloneCommand = `rclone copy ${rcloneFlags.join(" ")} "${backupPath}"`; + rcloneCommand = `rclone copy ${rcloneFlags.join(" ")} ${quote([backupPath])}`; } let credentials: DatabaseCredentials = {}; diff --git a/packages/server/src/utils/restore/libsql.ts b/packages/server/src/utils/restore/libsql.ts index f50f0c6da..9d7c7f15c 100644 --- a/packages/server/src/utils/restore/libsql.ts +++ b/packages/server/src/utils/restore/libsql.ts @@ -1,6 +1,7 @@ import type { apiRestoreBackup } from "@dokploy/server/db/schema"; import type { Destination } from "@dokploy/server/services/destination"; import type { Libsql } from "@dokploy/server/services/libsql"; +import { quote } from "shell-quote"; import type { z } from "zod"; import { getS3Credentials, getServiceContainerCommand } from "../backups/utils"; import { execAsync, execAsyncRemote } from "../process/execAsync"; @@ -19,7 +20,7 @@ export const restoreLibsqlBackup = async ( const backupPath = `${bucketPath}/${backupInput.backupFile}`; - const rcloneCommand = `rclone cat ${rcloneFlags.join(" ")} "${backupPath}"`; + const rcloneCommand = `rclone cat ${rcloneFlags.join(" ")} ${quote([backupPath])}`; const containerSearch = getServiceContainerCommand(appName); const restoreCommand = `docker exec -i $CONTAINER_ID sh -c "tar xzf - -C /var/lib/sqld"`; diff --git a/packages/server/src/utils/restore/mariadb.ts b/packages/server/src/utils/restore/mariadb.ts index afa167280..e4b301f46 100644 --- a/packages/server/src/utils/restore/mariadb.ts +++ b/packages/server/src/utils/restore/mariadb.ts @@ -1,6 +1,7 @@ import type { apiRestoreBackup } from "@dokploy/server/db/schema"; import type { Destination } from "@dokploy/server/services/destination"; import type { Mariadb } from "@dokploy/server/services/mariadb"; +import { quote } from "shell-quote"; import type { z } from "zod"; import { getS3Credentials } from "../backups/utils"; import { execAsync, execAsyncRemote } from "../process/execAsync"; @@ -19,7 +20,7 @@ export const restoreMariadbBackup = async ( const bucketPath = `:s3:${destination.bucket}`; const backupPath = `${bucketPath}/${backupInput.backupFile}`; - const rcloneCommand = `rclone cat ${rcloneFlags.join(" ")} "${backupPath}" | gunzip`; + const rcloneCommand = `rclone cat ${rcloneFlags.join(" ")} ${quote([backupPath])} | gunzip`; const command = getRestoreCommand({ appName, diff --git a/packages/server/src/utils/restore/mongo.ts b/packages/server/src/utils/restore/mongo.ts index 91eb9af5a..163340ca8 100644 --- a/packages/server/src/utils/restore/mongo.ts +++ b/packages/server/src/utils/restore/mongo.ts @@ -1,6 +1,7 @@ import type { apiRestoreBackup } from "@dokploy/server/db/schema"; import type { Destination } from "@dokploy/server/services/destination"; import type { Mongo } from "@dokploy/server/services/mongo"; +import { quote } from "shell-quote"; import type { z } from "zod"; import { getS3Credentials } from "../backups/utils"; import { execAsync, execAsyncRemote } from "../process/execAsync"; @@ -18,7 +19,7 @@ export const restoreMongoBackup = async ( const rcloneFlags = getS3Credentials(destination); const bucketPath = `:s3:${destination.bucket}`; const backupPath = `${bucketPath}/${backupInput.backupFile}`; - const rcloneCommand = `rclone copy ${rcloneFlags.join(" ")} "${backupPath}"`; + const rcloneCommand = `rclone copy ${rcloneFlags.join(" ")} ${quote([backupPath])}`; const command = getRestoreCommand({ appName, diff --git a/packages/server/src/utils/restore/mysql.ts b/packages/server/src/utils/restore/mysql.ts index c955ef960..d237da895 100644 --- a/packages/server/src/utils/restore/mysql.ts +++ b/packages/server/src/utils/restore/mysql.ts @@ -1,6 +1,7 @@ import type { apiRestoreBackup } from "@dokploy/server/db/schema"; import type { Destination } from "@dokploy/server/services/destination"; import type { MySql } from "@dokploy/server/services/mysql"; +import { quote } from "shell-quote"; import type { z } from "zod"; import { getS3Credentials } from "../backups/utils"; import { execAsync, execAsyncRemote } from "../process/execAsync"; @@ -19,7 +20,7 @@ export const restoreMySqlBackup = async ( const bucketPath = `:s3:${destination.bucket}`; const backupPath = `${bucketPath}/${backupInput.backupFile}`; - const rcloneCommand = `rclone cat ${rcloneFlags.join(" ")} "${backupPath}" | gunzip`; + const rcloneCommand = `rclone cat ${rcloneFlags.join(" ")} ${quote([backupPath])} | gunzip`; const command = getRestoreCommand({ appName, diff --git a/packages/server/src/utils/restore/postgres.ts b/packages/server/src/utils/restore/postgres.ts index 8cbbe2175..05dcf38a3 100644 --- a/packages/server/src/utils/restore/postgres.ts +++ b/packages/server/src/utils/restore/postgres.ts @@ -1,6 +1,7 @@ import type { apiRestoreBackup } from "@dokploy/server/db/schema"; import type { Destination } from "@dokploy/server/services/destination"; import type { Postgres } from "@dokploy/server/services/postgres"; +import { quote } from "shell-quote"; import type { z } from "zod"; import { getS3Credentials } from "../backups/utils"; import { execAsync, execAsyncRemote } from "../process/execAsync"; @@ -20,7 +21,7 @@ export const restorePostgresBackup = async ( const backupPath = `${bucketPath}/${backupInput.backupFile}`; - const rcloneCommand = `rclone cat ${rcloneFlags.join(" ")} "${backupPath}" | gunzip`; + const rcloneCommand = `rclone cat ${rcloneFlags.join(" ")} ${quote([backupPath])} | gunzip`; const command = getRestoreCommand({ appName, diff --git a/packages/server/src/utils/restore/utils.ts b/packages/server/src/utils/restore/utils.ts index 18065fb99..dd2934430 100644 --- a/packages/server/src/utils/restore/utils.ts +++ b/packages/server/src/utils/restore/utils.ts @@ -92,8 +92,8 @@ rm -rf ${tempDir} && \ mkdir -p ${tempDir} && \ ${rcloneCommand} ${tempDir} && \ cd ${tempDir} && \ -gunzip -f "${fileName}" && \ -${restoreCommand} < "${decompressedName}" && \ +gunzip -f ${quote([fileName])} && \ +${restoreCommand} < ${quote([decompressedName])} && \ rm -rf ${tempDir} `; }; diff --git a/packages/server/src/utils/restore/web-server.ts b/packages/server/src/utils/restore/web-server.ts index 683a1898a..afcd81cf6 100644 --- a/packages/server/src/utils/restore/web-server.ts +++ b/packages/server/src/utils/restore/web-server.ts @@ -3,6 +3,7 @@ import { tmpdir } from "node:os"; 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 { getS3Credentials } from "../backups/utils"; import { execAsync } from "../process/execAsync"; @@ -35,7 +36,7 @@ export const restoreWebServerBackup = async ( // Download backup from S3 emit("Downloading backup from S3..."); await execAsync( - `rclone copyto ${rcloneFlags.join(" ")} "${backupPath}" "${tempDir}/${backupFile}"`, + `rclone copyto ${rcloneFlags.join(" ")} ${quote([backupPath])} ${quote([`${tempDir}/${backupFile}`])}`, ); // List files before extraction @@ -45,7 +46,9 @@ export const restoreWebServerBackup = async ( // Extract backup emit("Extracting backup..."); - await execAsync(`cd ${tempDir} && unzip ${backupFile} > /dev/null 2>&1`); + await execAsync( + `cd ${quote([tempDir])} && unzip ${quote([backupFile])} > /dev/null 2>&1`, + ); // Restore filesystem first emit("Restoring filesystem..."); diff --git a/packages/server/src/utils/schedules/utils.ts b/packages/server/src/utils/schedules/utils.ts index 64657c9a6..8107839d8 100644 --- a/packages/server/src/utils/schedules/utils.ts +++ b/packages/server/src/utils/schedules/utils.ts @@ -9,6 +9,7 @@ import { } from "@dokploy/server/services/deployment"; import { findScheduleById } from "@dokploy/server/services/schedule"; import { scheduledJobs, scheduleJob as scheduleJobNode } from "node-schedule"; +import { quote } from "shell-quote"; import { getComposeContainer, getServiceContainer } from "../docker/utils"; import { execAsyncRemote } from "../process/execAsync"; import { spawnAsync } from "../process/spawnAsync"; @@ -77,12 +78,12 @@ export const runCommand = async (scheduleId: string) => { serverId, ` set -e - echo "Running command: docker exec ${containerId} ${shellType} -c '${command}'" >> ${deployment.logPath}; - docker exec ${containerId} ${shellType} -c '${command}' >> ${deployment.logPath} 2>> ${deployment.logPath} || { - echo "❌ Command failed" >> ${deployment.logPath}; + echo "Running scheduled command" >> ${quote([deployment.logPath])}; + docker exec ${quote([containerId])} ${quote([shellType])} -c ${quote([command])} >> ${quote([deployment.logPath])} 2>> ${quote([deployment.logPath])} || { + echo "❌ Command failed" >> ${quote([deployment.logPath])}; exit 1; } - echo "✅ Command executed successfully" >> ${deployment.logPath}; + echo "✅ Command executed successfully" >> ${quote([deployment.logPath])}; `, ); } catch (error) { diff --git a/packages/server/src/utils/traefik/application.ts b/packages/server/src/utils/traefik/application.ts index 101574ed5..3da847f59 100644 --- a/packages/server/src/utils/traefik/application.ts +++ b/packages/server/src/utils/traefik/application.ts @@ -3,6 +3,7 @@ import path from "node:path"; import { createInterface } from "node:readline"; import { paths } from "@dokploy/server/constants"; import type { Domain } from "@dokploy/server/services/domain"; +import { quote } from "shell-quote"; import { parse, stringify } from "yaml"; import { encodeBase64 } from "../docker/utils"; import { execAsync, execAsyncRemote } from "../process/execAsync"; @@ -57,7 +58,7 @@ export const removeTraefikConfig = async ( try { const { DYNAMIC_TRAEFIK_PATH } = paths(!!serverId); const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`); - const command = `rm -f ${configPath}`; + const command = `rm -f ${quote([configPath])}`; if (serverId) { await execAsyncRemote(serverId, command); @@ -76,7 +77,7 @@ export const removeTraefikConfigRemote = async ( try { const { DYNAMIC_TRAEFIK_PATH } = paths(true); const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`); - await execAsyncRemote(serverId, `rm -f ${configPath}`); + await execAsyncRemote(serverId, `rm -f ${quote([configPath])}`); } catch (error) { console.error( `Error removing remote traefik config for ${appName}:`, @@ -106,7 +107,10 @@ export const loadOrCreateConfigRemote = async ( const fileConfig: FileConfig = { http: { routers: {}, services: {} } }; const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`); try { - const { stdout } = await execAsyncRemote(serverId, `cat ${configPath}`); + const { stdout } = await execAsyncRemote( + serverId, + `cat ${quote([configPath])}`, + ); if (!stdout) return fileConfig; @@ -133,7 +137,10 @@ export const readRemoteConfig = async (serverId: string, appName: string) => { const { DYNAMIC_TRAEFIK_PATH } = paths(true); const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`); try { - const { stdout } = await execAsyncRemote(serverId, `cat ${configPath}`); + const { stdout } = await execAsyncRemote( + serverId, + `cat ${quote([configPath])}`, + ); if (!stdout) return null; return stdout; } catch { @@ -189,7 +196,10 @@ export const readConfigInPath = async (pathFile: string, serverId?: string) => { const configPath = path.join(pathFile); if (serverId) { - const { stdout } = await execAsyncRemote(serverId, `cat ${configPath}`); + const { stdout } = await execAsyncRemote( + serverId, + `cat ${quote([configPath])}`, + ); if (!stdout) return null; return stdout; } @@ -221,7 +231,7 @@ export const writeConfigRemote = async ( const encoded = encodeBase64(traefikConfig); await execAsyncRemote( serverId, - `echo "${encoded}" | base64 -d > "${configPath}"`, + `echo "${encoded}" | base64 -d > ${quote([configPath])}`, ); } catch (e) { console.error("Error saving the YAML config file:", e); @@ -239,7 +249,7 @@ export const writeTraefikConfigInPath = async ( const encoded = encodeBase64(traefikConfig); await execAsyncRemote( serverId, - `echo "${encoded}" | base64 -d > "${configPath}"`, + `echo "${encoded}" | base64 -d > ${quote([configPath])}`, ); } else { fs.writeFileSync(configPath, traefikConfig, "utf8"); @@ -272,7 +282,11 @@ export const writeTraefikConfigRemote = async ( const { DYNAMIC_TRAEFIK_PATH } = paths(true); const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`); const yamlStr = stringify(traefikConfig); - await execAsyncRemote(serverId, `echo '${yamlStr}' > ${configPath}`); + const encoded = encodeBase64(yamlStr); + await execAsyncRemote( + serverId, + `echo "${encoded}" | base64 -d > ${quote([configPath])}`, + ); } catch (e) { console.error("Error saving the YAML config file:", e); } diff --git a/packages/server/src/utils/volume-backups/restore.ts b/packages/server/src/utils/volume-backups/restore.ts index 6f6068caf..812f6cefd 100644 --- a/packages/server/src/utils/volume-backups/restore.ts +++ b/packages/server/src/utils/volume-backups/restore.ts @@ -1,4 +1,5 @@ import path from "node:path"; +import { quote } from "shell-quote"; import { findApplicationById, findComposeById, @@ -23,7 +24,7 @@ export const restoreVolume = async ( const backupPath = `${bucketPath}/${backupFileName}`; // Command to download backup file from S3 - const downloadCommand = `rclone copyto ${rcloneFlags.join(" ")} "${backupPath}" "${volumeBackupPath}/${backupFileName}"`; + const downloadCommand = `rclone copyto ${rcloneFlags.join(" ")} ${quote([backupPath])} ${quote([`${volumeBackupPath}/${backupFileName}`])}`; // Base restore command that creates the volume and restores data const baseRestoreCommand = ` @@ -40,7 +41,7 @@ export const restoreVolume = async ( -v ${volumeName}:/volume_data \ -v ${volumeBackupPath}:/backup \ ubuntu \ - bash -c "cd /volume_data && tar xvf /backup/${backupFileName} ." + bash -c "cd /volume_data && tar xvf /backup/${quote([backupFileName])} ." echo "Volume restore completed ✅" `;