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/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/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 ✅" `;