mirror of
https://github.com/andwati/dokploy.git
synced 2026-11-10 22:15:16 +00:00
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:
@@ -13,6 +13,8 @@ import { db } from "@dokploy/server/db";
|
|||||||
import {
|
import {
|
||||||
createVolumeBackupSchema,
|
createVolumeBackupSchema,
|
||||||
updateVolumeBackupSchema,
|
updateVolumeBackupSchema,
|
||||||
|
VOLUME_NAME_MESSAGE,
|
||||||
|
VOLUME_NAME_REGEX,
|
||||||
volumeBackups,
|
volumeBackups,
|
||||||
} from "@dokploy/server/db/schema";
|
} from "@dokploy/server/db/schema";
|
||||||
import { findDestinationById } from "@dokploy/server/services/destination";
|
import { findDestinationById } from "@dokploy/server/services/destination";
|
||||||
@@ -275,7 +277,10 @@ export const volumeBackupsRouter = createTRPCRouter({
|
|||||||
z.object({
|
z.object({
|
||||||
backupFileName: z.string().min(1),
|
backupFileName: z.string().min(1),
|
||||||
destinationId: 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),
|
id: z.string().min(1),
|
||||||
serviceType: z.enum(["application", "compose"]),
|
serviceType: z.enum(["application", "compose"]),
|
||||||
serverId: z.string().optional(),
|
serverId: z.string().optional(),
|
||||||
|
|||||||
@@ -41,6 +41,12 @@ export const APP_NAME_REGEX = /^[a-zA-Z0-9._-]+$/;
|
|||||||
export const APP_NAME_MESSAGE =
|
export const APP_NAME_MESSAGE =
|
||||||
"App name can only contain letters, numbers, dots, underscores and hyphens";
|
"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. */
|
/** Database password: blocks shell-dangerous characters like $ ! ' " \ / and spaces. */
|
||||||
export const DATABASE_PASSWORD_REGEX =
|
export const DATABASE_PASSWORD_REGEX =
|
||||||
/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/;
|
/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/;
|
||||||
|
|||||||
@@ -14,7 +14,11 @@ import { serviceType } from "./mount";
|
|||||||
import { mysql } from "./mysql";
|
import { mysql } from "./mysql";
|
||||||
import { postgres } from "./postgres";
|
import { postgres } from "./postgres";
|
||||||
import { redis } from "./redis";
|
import { redis } from "./redis";
|
||||||
import { generateAppName } from "./utils";
|
import {
|
||||||
|
generateAppName,
|
||||||
|
VOLUME_NAME_MESSAGE,
|
||||||
|
VOLUME_NAME_REGEX,
|
||||||
|
} from "./utils";
|
||||||
|
|
||||||
export const volumeBackups = pgTable("volume_backup", {
|
export const volumeBackups = pgTable("volume_backup", {
|
||||||
volumeBackupId: text("volumeBackupId")
|
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,
|
volumeBackupId: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { deployMySql } from "@dokploy/server/services/mysql";
|
|||||||
import { deployPostgres } from "@dokploy/server/services/postgres";
|
import { deployPostgres } from "@dokploy/server/services/postgres";
|
||||||
import { deployRedis } from "@dokploy/server/services/redis";
|
import { deployRedis } from "@dokploy/server/services/redis";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
import { quote } from "shell-quote";
|
||||||
import { removeService } from "../docker/utils";
|
import { removeService } from "../docker/utils";
|
||||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ export const rebuildDatabase = async (
|
|||||||
|
|
||||||
for (const mount of database.mounts) {
|
for (const mount of database.mounts) {
|
||||||
if (mount.type === "volume") {
|
if (mount.type === "volume") {
|
||||||
const command = `docker volume rm ${mount?.volumeName} --force`;
|
const command = `docker volume rm ${quote([mount?.volumeName ?? ""])} --force`;
|
||||||
if (database.serverId) {
|
if (database.serverId) {
|
||||||
await execAsyncRemote(database.serverId, command);
|
await execAsyncRemote(database.serverId, command);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import { quote } from "shell-quote";
|
||||||
import {
|
import {
|
||||||
findApplicationById,
|
findApplicationById,
|
||||||
findComposeById,
|
findComposeById,
|
||||||
@@ -23,7 +24,7 @@ export const restoreVolume = async (
|
|||||||
const backupPath = `${bucketPath}/${backupFileName}`;
|
const backupPath = `${bucketPath}/${backupFileName}`;
|
||||||
|
|
||||||
// Command to download backup file from S3
|
// 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
|
// Base restore command that creates the volume and restores data
|
||||||
const baseRestoreCommand = `
|
const baseRestoreCommand = `
|
||||||
@@ -40,7 +41,7 @@ export const restoreVolume = async (
|
|||||||
-v ${volumeName}:/volume_data \
|
-v ${volumeName}:/volume_data \
|
||||||
-v ${volumeBackupPath}:/backup \
|
-v ${volumeBackupPath}:/backup \
|
||||||
ubuntu \
|
ubuntu \
|
||||||
bash -c "cd /volume_data && tar xvf /backup/${backupFileName} ."
|
bash -c "cd /volume_data && tar xvf /backup/${quote([backupFileName])} ."
|
||||||
echo "Volume restore completed ✅"
|
echo "Volume restore completed ✅"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user