fix(settings): confine Traefik file read/write to the Traefik config directory

This commit is contained in:
Bunlong Heng 2026-08-12 17:38:48 -04:00
parent 561fb43d42
commit 1f1acacf58

View File

@ -1,3 +1,4 @@
import path from "node:path";
import {
CLEANUP_CRON_JOB,
checkGPUStatus,
@ -81,6 +82,32 @@ import {
publicProcedure,
} from "../trpc";
/**
* Confines a caller-supplied Traefik file path to the Traefik configuration
* directory (MAIN_TRAEFIK_PATH). The read/write Traefik file endpoints only
* expose files listed under this directory, but the raw path was passed
* straight to the filesystem helpers, letting a user with the `traefikFiles`
* permission read or overwrite arbitrary files on the host (for example
* `/etc/dokploy/.env` or `~/.ssh/authorized_keys`) via an absolute path or
* `../` traversal. Reject anything that escapes the directory.
*/
const resolveTraefikFilePath = (
filePath: string,
serverId?: string | null,
): string => {
const { MAIN_TRAEFIK_PATH } = paths(!!serverId);
const base = path.resolve(MAIN_TRAEFIK_PATH);
const resolved = path.resolve(base, filePath);
if (resolved !== base && !resolved.startsWith(`${base}${path.sep}`)) {
throw new TRPCError({
code: "BAD_REQUEST",
message:
"Invalid path: file must be inside the Traefik configuration directory",
});
}
return resolved;
};
export const settingsRouter = createTRPCRouter({
getWebServerSettings: protectedProcedure.query(async () => {
if (IS_CLOUD) {
@ -598,8 +625,9 @@ export const settingsRouter = createTRPCRouter({
.input(apiModifyTraefikConfig)
.mutation(async ({ input, ctx }) => {
await checkPermission(ctx, { traefikFiles: ["write"] });
const safePath = resolveTraefikFilePath(input.path, input?.serverId);
await writeTraefikConfigInPath(
input.path,
safePath,
input.traefikConfig,
input?.serverId,
);
@ -624,7 +652,8 @@ export const settingsRouter = createTRPCRouter({
}
}
return readConfigInPath(input.path, input.serverId);
const safePath = resolveTraefikFilePath(input.path, input.serverId);
return readConfigInPath(safePath, input.serverId);
}),
getIp: protectedProcedure.query(async () => {
if (IS_CLOUD) {