fix(security): validate volumeName and escape volume-backup file names

Add VOLUME_NAME_REGEX (Docker volume-name format) and enforce it on volumeName in
create/update/runManually — a legit volume name never contains shell metacharacters,
so this blocks injection across every docker run/rm/rclone sink at once. Escape the
user-supplied backupFileName and the mount volumeName in database rebuild with quote().
This commit is contained in:
Mauricio Siu
2026-07-20 16:44:53 -06:00
parent eeb6e7b8ea
commit d629faebc6
5 changed files with 25 additions and 6 deletions

View File

@@ -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(),

View File

@@ -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@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/;

View File

@@ -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,
});

View File

@@ -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 {

View File

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