Compare commits

...

2 Commits

Author SHA1 Message Date
Mauricio Siu
fbf2ff73c2 fix(security): stop user-supplied certificatePath in create schema
Same drizzle-zod $defaultFn-as-optional leak as schedule appName: certificatePath
(notNull + $defaultFn) was overridable, enabling path traversal out of the certs
dir even with the escaped shell paths. Omit it from apiCreateCertificate so it is
always server-generated (completes GHSA-q9qw).
2026-07-20 17:08:32 -06:00
Mauricio Siu
6943a395fe fix(security): stop user-supplied schedule appName and escape its shell paths
drizzle-zod exposed appName (notNull + $defaultFn) as optional in the insert
schema, letting callers override it; it then flowed unquoted into rm -rf/mkdir
paths. Omit appName from createScheduleSchema and quote() the schedule fullPath
sinks (completes GHSA-fgh8 VULN-015 alongside the mount/patch fixes).
2026-07-20 17:07:32 -06:00
3 changed files with 10 additions and 8 deletions

View File

@@ -45,7 +45,7 @@ export const apiCreateCertificate = createInsertSchema(certificates, {
privateKey: z.string().min(1), privateKey: z.string().min(1),
autoRenew: z.boolean().optional(), autoRenew: z.boolean().optional(),
serverId: z.string().optional(), serverId: z.string().optional(),
}); }).omit({ certificatePath: true });
export const apiFindCertificate = z.object({ export const apiFindCertificate = z.object({
certificateId: z.string().min(1), certificateId: z.string().min(1),

View File

@@ -81,7 +81,7 @@ export const schedulesRelations = relations(schedules, ({ one, many }) => ({
export const createScheduleSchema = createInsertSchema(schedules, { export const createScheduleSchema = createInsertSchema(schedules, {
scheduleType: z.enum(["application", "compose", "server", "dokploy-server"]), scheduleType: z.enum(["application", "compose", "server", "dokploy-server"]),
}); }).omit({ appName: true });
export const updateScheduleSchema = createScheduleSchema.extend({ export const updateScheduleSchema = createScheduleSchema.extend({
scheduleId: z.string().min(1), scheduleId: z.string().min(1),

View File

@@ -1,6 +1,7 @@
import path from "node:path"; import path from "node:path";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { quote } from "shell-quote";
import type { z } from "zod"; import type { z } from "zod";
import { IS_CLOUD, paths } from "../constants"; import { IS_CLOUD, paths } from "../constants";
import { db } from "../db"; import { db } from "../db";
@@ -142,7 +143,7 @@ export const deleteSchedule = async (scheduleId: string) => {
const { SCHEDULES_PATH } = paths(!!serverId); const { SCHEDULES_PATH } = paths(!!serverId);
const fullPath = path.join(SCHEDULES_PATH, schedule?.appName || ""); const fullPath = path.join(SCHEDULES_PATH, schedule?.appName || "");
const command = `rm -rf ${fullPath}`; const command = `rm -rf ${quote([fullPath])}`;
if (serverId) { if (serverId) {
await execAsyncRemote(serverId, command); await execAsyncRemote(serverId, command);
} else { } else {
@@ -198,12 +199,13 @@ const handleScript = async (schedule: Schedule) => {
${schedule?.script || ""}`; ${schedule?.script || ""}`;
const encodedContent = encodeBase64(scriptWithPid); const encodedContent = encodeBase64(scriptWithPid);
const scriptPath = `${fullPath}/script.sh`;
const script = ` const script = `
mkdir -p ${fullPath} mkdir -p ${quote([fullPath])}
rm -f ${fullPath}/script.sh rm -f ${quote([scriptPath])}
touch ${fullPath}/script.sh touch ${quote([scriptPath])}
chmod +x ${fullPath}/script.sh chmod +x ${quote([scriptPath])}
echo "${encodedContent}" | base64 -d > ${fullPath}/script.sh echo "${encodedContent}" | base64 -d > ${quote([scriptPath])}
`; `;
if (schedule?.scheduleType === "dokploy-server") { if (schedule?.scheduleType === "dokploy-server") {