From d1c33285318ecb882f478d82d1355817b02b2544 Mon Sep 17 00:00:00 2001 From: tuanaiseo Date: Fri, 24 Apr 2026 06:09:42 +0700 Subject: [PATCH] fix(security)(core): path safety check is vulnerable to symlink-based d `isSafePath` only verifies string prefix containment after `resolve()`. This does not account for symlinks inside the project directory that point outside the base path. Downstream file reads/writes that rely on this helper can be tricked into accessing files outside the intended project root via symlink traversal. Affected files: safePath.ts Signed-off-by: tuanaiseo <221258316+tuanaiseo@users.noreply.github.com> --- .../core/src/studio-api/helpers/safePath.ts | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/packages/core/src/studio-api/helpers/safePath.ts b/packages/core/src/studio-api/helpers/safePath.ts index 7a925c3c6..ff515875b 100644 --- a/packages/core/src/studio-api/helpers/safePath.ts +++ b/packages/core/src/studio-api/helpers/safePath.ts @@ -1,10 +1,33 @@ -import { resolve, sep, join } from "node:path"; -import { readdirSync } from "node:fs"; +import { resolve, sep, join, dirname, basename } from "node:path"; +import { readdirSync, realpathSync } from "node:fs"; /** Reject paths that escape the project directory. */ export function isSafePath(base: string, resolved: string): boolean { - const norm = resolve(base) + sep; - return resolved.startsWith(norm) || resolved === resolve(base); + try { + const baseReal = realpathSync(resolve(base)); + const target = resolve(resolved); + let probe = target; + const segments: string[] = []; + let targetReal: string; + + while (true) { + try { + const real = realpathSync(probe); + targetReal = segments.length ? join(real, ...segments.reverse()) : real; + break; + } catch { + const parent = dirname(probe); + if (parent === probe) return false; + segments.push(basename(probe)); + probe = parent; + } + } + + const norm = baseReal + sep; + return targetReal.startsWith(norm) || targetReal === baseReal; + } catch { + return false; + } } const IGNORE_DIRS = new Set([".thumbnails", "node_modules", ".git"]);