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
6 changed files with 14 additions and 28 deletions

View File

@@ -648,7 +648,7 @@ function SidebarLogo() {
</SidebarMenuButton> </SidebarMenuButton>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent
className="w-64 rounded-lg max-h-[min(70vh,28rem)] flex flex-col" className="rounded-lg max-h-[min(70vh,28rem)] flex flex-col"
align="start" align="start"
side={isMobile ? "bottom" : "right"} side={isMobile ? "bottom" : "right"}
sideOffset={4} sideOffset={4}

View File

@@ -5,7 +5,6 @@ import {
findRegistryById, findRegistryById,
IS_CLOUD, IS_CLOUD,
removeRegistry, removeRegistry,
safeDockerLoginCommand,
updateRegistry, updateRegistry,
} from "@dokploy/server"; } from "@dokploy/server";
import { db } from "@dokploy/server/db"; import { db } from "@dokploy/server/db";
@@ -123,11 +122,7 @@ export const registryRouter = createTRPCRouter({
if (input.serverId && input.serverId !== "none") { if (input.serverId && input.serverId !== "none") {
await execAsyncRemote( await execAsyncRemote(
input.serverId, input.serverId,
safeDockerLoginCommand( `echo ${input.password} | docker ${args.join(" ")}`,
input.registryUrl,
input.username,
input.password,
),
); );
} else { } else {
await execFileAsync("docker", args, { await execFileAsync("docker", args, {
@@ -187,11 +182,7 @@ export const registryRouter = createTRPCRouter({
if (input.serverId && input.serverId !== "none") { if (input.serverId && input.serverId !== "none") {
await execAsyncRemote( await execAsyncRemote(
input.serverId, input.serverId,
safeDockerLoginCommand( `echo ${registryData.password} | docker ${args.join(" ")}`,
registryData.registryUrl,
registryData.username,
registryData.password,
),
); );
} else { } else {
await execFileAsync("docker", args, { await execFileAsync("docker", args, {

View File

@@ -413,14 +413,6 @@ export const serverRouter = createTRPCRouter({
.input(apiRemoveServer) .input(apiRemoveServer)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
const currentServer = await findServerById(input.serverId);
if (currentServer.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to delete this server",
});
}
const activeServers = await haveActiveServices(input.serverId); const activeServers = await haveActiveServices(input.serverId);
if (activeServers) { if (activeServers) {
@@ -429,6 +421,7 @@ export const serverRouter = createTRPCRouter({
message: "Server has active services, please delete them first", message: "Server has active services, please delete them first",
}); });
} }
const currentServer = await findServerById(input.serverId);
await audit(ctx, { await audit(ctx, {
action: "delete", action: "delete",
resourceType: "server", resourceType: "server",

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") {