mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-10-16 13:45:28 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 18fb2547e4 |
@@ -12,12 +12,14 @@ import {
|
||||
findPostgresById,
|
||||
findRedisById,
|
||||
getServiceContainer,
|
||||
isDangerousBindMountPath,
|
||||
updateMount,
|
||||
} from "@dokploy/server";
|
||||
import type { ServiceType } from "@dokploy/server/db/schema/mount";
|
||||
import {
|
||||
checkServiceAccess,
|
||||
checkServicePermissionAndAccess,
|
||||
isPrivilegedOrgRole,
|
||||
} from "@dokploy/server/services/permission";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
@@ -73,6 +75,23 @@ async function getServiceOrganizationId(
|
||||
}
|
||||
}
|
||||
|
||||
async function assertSafeBindMountPath(
|
||||
ctx: { session: { userId: string; activeOrganizationId: string } },
|
||||
type: string | undefined,
|
||||
hostPath: string | null | undefined,
|
||||
) {
|
||||
if (type !== "bind" || !hostPath) return;
|
||||
if (!isDangerousBindMountPath(hostPath)) return;
|
||||
|
||||
if (!(await isPrivilegedOrgRole(ctx.session))) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message:
|
||||
"This host path is reserved and can't be used in a bind mount. Only an organization owner or admin can do that.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const mountRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.input(apiCreateMount)
|
||||
@@ -80,6 +99,7 @@ export const mountRouter = createTRPCRouter({
|
||||
await checkServicePermissionAndAccess(ctx, input.serviceId, {
|
||||
volume: ["create"],
|
||||
});
|
||||
await assertSafeBindMountPath(ctx, input.type, input.hostPath);
|
||||
const mount = await createMount(input);
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
@@ -153,6 +173,11 @@ export const mountRouter = createTRPCRouter({
|
||||
volume: ["create"],
|
||||
});
|
||||
}
|
||||
await assertSafeBindMountPath(
|
||||
ctx,
|
||||
input.type ?? mount.type,
|
||||
input.hostPath ?? mount.hostPath,
|
||||
);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "mount",
|
||||
|
||||
@@ -23,6 +23,31 @@ import type { z } from "zod";
|
||||
|
||||
export type Mount = typeof mounts.$inferSelect;
|
||||
|
||||
// Absolute host paths a "bind" mount must never be allowed to target for a
|
||||
// non-owner/admin caller - each one gives a way off the container onto the
|
||||
// Dokploy control host (docker.sock -> full daemon control, /etc/dokploy ->
|
||||
// Dokploy's own config/certs, /root //boot/proc/sys -> host takeover).
|
||||
const DANGEROUS_HOST_PATH_PREFIXES = [
|
||||
"/var/run/docker.sock",
|
||||
"/run/docker.sock",
|
||||
"/var/lib/docker",
|
||||
"/etc/dokploy",
|
||||
"/root",
|
||||
"/boot",
|
||||
"/proc",
|
||||
"/sys",
|
||||
];
|
||||
|
||||
export const isDangerousBindMountPath = (hostPath: string): boolean => {
|
||||
// Resolve against "/" so relative segments and ".." traversal tricks
|
||||
// (e.g. "../../var/run/docker.sock") can't sneak past the prefix check.
|
||||
const normalized = path.posix.resolve("/", hostPath.trim());
|
||||
if (normalized === "/") return true;
|
||||
return DANGEROUS_HOST_PATH_PREFIXES.some(
|
||||
(prefix) => normalized === prefix || normalized.startsWith(`${prefix}/`),
|
||||
);
|
||||
};
|
||||
|
||||
export const createMount = async (input: z.infer<typeof apiCreateMount>) => {
|
||||
try {
|
||||
const { serviceId, ...rest } = input;
|
||||
|
||||
@@ -434,3 +434,22 @@ export const findMemberByUserId = async (
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// Some actions (e.g. a bind mount targeting a sensitive host path) must stay
|
||||
// restricted to the org's owner/admin even if a custom role was granted the
|
||||
// underlying resource permission (e.g. volume:create) - that permission only
|
||||
// covers ordinary use, not host takeover.
|
||||
export const isPrivilegedOrgRole = async (session: {
|
||||
userId: string;
|
||||
activeOrganizationId: string;
|
||||
}): Promise<boolean> => {
|
||||
const memberRecord = await db.query.member.findFirst({
|
||||
where: and(
|
||||
eq(member.userId, session.userId),
|
||||
eq(member.organizationId, session.activeOrganizationId),
|
||||
),
|
||||
columns: { role: true },
|
||||
});
|
||||
|
||||
return memberRecord?.role === "owner" || memberRecord?.role === "admin";
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user