From 6731abfa43eb96bb7f32bb5ccb89b996a99e024b Mon Sep 17 00:00:00 2001 From: Test User Date: Tue, 5 May 2026 08:45:36 +0800 Subject: [PATCH] fix(security): add path traversal protection to render file serving The /projects/:id/renders/file/* endpoint served files by joining rendersDir with the URL filename without validating the resolved path stays within rendersDir. A request to /renders/file/../../etc/passwd would serve arbitrary files. Add a ".." check on the filename and verify the resolved path starts with the renders directory. Co-Authored-By: Claude Opus 4.7 --- packages/core/src/studio-api/routes/render.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core/src/studio-api/routes/render.ts b/packages/core/src/studio-api/routes/render.ts index a84b1943d..f48556d08 100644 --- a/packages/core/src/studio-api/routes/render.ts +++ b/packages/core/src/studio-api/routes/render.ts @@ -1,7 +1,7 @@ import type { Hono } from "hono"; import { streamSSE } from "hono/streaming"; import { existsSync, readFileSync, mkdirSync, unlinkSync, readdirSync, statSync } from "node:fs"; -import { join } from "node:path"; +import { join, resolve } from "node:path"; import type { StudioApiAdapter, RenderJobState } from "../types.js"; export function registerRenderRoutes(api: Hono, adapter: StudioApiAdapter): void { @@ -182,8 +182,10 @@ export function registerRenderRoutes(api: Hono, adapter: StudioApiAdapter): void if (!project) return c.json({ error: "not found" }, 404); const filename = c.req.path.split("/renders/file/")[1]; if (!filename) return c.json({ error: "missing filename" }, 400); + if (filename.includes("..")) return c.json({ error: "forbidden" }, 403); const rendersDir = adapter.rendersDir(project); const fp = join(rendersDir, filename); + if (!fp.startsWith(resolve(rendersDir))) return c.json({ error: "forbidden" }, 403); if (!existsSync(fp)) return c.json({ error: "not found" }, 404); const contentType = renderContentType(fp); const content = readFileSync(fp);