From b3a7560a6c81a4b8142bc2ebcaa089894dc4b7c5 Mon Sep 17 00:00:00 2001 From: npmrun <1549469775@qq.com> Date: Thu, 7 May 2026 23:32:14 +0800 Subject: [PATCH] refactor(media): improve directory handling and path normalization - Updated functions to allow both relative and absolute paths, enhancing flexibility in directory configuration. - Renamed `ensureRelativeDir` to `ensureConfigurableDir` for clarity and updated its implementation to normalize paths and prevent directory traversal. - Adjusted related constants to reflect the new path handling logic, ensuring consistent behavior across the application. --- server/constants/media.ts | 30 +++++++++++++++++------------- server/middleware/00.public.ts | 7 ++++--- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/server/constants/media.ts b/server/constants/media.ts index 4eda20e..d57c25d 100644 --- a/server/constants/media.ts +++ b/server/constants/media.ts @@ -1,3 +1,5 @@ +import path from "node:path"; + function trimSlashes(input: string): string { return input.trim().replace(/^\/+|\/+$/g, ""); } @@ -9,16 +11,18 @@ function hasParentSegment(input: string): boolean { .some((part) => part === ".."); } -function ensureRelativeDir(input: string, fallback: string, envName: string): string { - const value = input.trim(); - if (!value) { +/** 允许相对项目根或绝对路径;禁止含 `..` 的路径片段(防止配置逃逸)。 */ +function ensureConfigurableDir(input: string, fallback: string, envName: string): string { + const raw = input.trim(); + if (!raw) { return fallback; } - // 仅允许相对目录;绝对路径会绕过项目根约束。 - if (value.startsWith("/")) { - throw new Error(`${envName} must be a relative directory path`); + const normalized = path.normalize(raw); + const segments = normalized.split(path.sep); + if (segments.some((part) => part === "..")) { + throw new Error(`${envName} must not contain ".." path segments`); } - return value; + return normalized; } function ensureSafeSubdir(input: string, fallback: string, envName: string): string { @@ -35,8 +39,8 @@ function ensureSafeSubdir(input: string, fallback: string, envName: string): str /** 静态资源 URL 前缀固定为 `/static`,不允许通过环境变量覆写。 */ export const STATIC_PUBLIC_PREFIX = "/static"; -/** 静态资源根目录,默认 `static` */ -export const STATIC_DIR = ensureRelativeDir(process.env.STATIC_DIR ?? "static", "static", "STATIC_DIR"); +/** 静态资源根目录(相对项目根或绝对路径),默认 `static` */ +export const STATIC_DIR = ensureConfigurableDir(process.env.STATIC_DIR ?? "static", "static", "STATIC_DIR"); /** 媒体上传子目录(相对 STATIC_DIR),默认 `media` */ export const MEDIA_UPLOAD_SUBDIR = ensureSafeSubdir( @@ -45,11 +49,11 @@ export const MEDIA_UPLOAD_SUBDIR = ensureSafeSubdir( "MEDIA_UPLOAD_SUBDIR", ); -/** 媒体上传目录(相对项目根),默认 `static/media` */ -export const RELATIVE_ASSETS_DIR = `${STATIC_DIR}/${MEDIA_UPLOAD_SUBDIR}`; +/** 媒体上传目录(与 STATIC_DIR 同为相对或绝对),默认 `static/media` */ +export const RELATIVE_ASSETS_DIR = path.join(STATIC_DIR, MEDIA_UPLOAD_SUBDIR); -/** 临时目录(相对项目根),默认 `.tmp` */ -export const RELATIVE_TMP_DIR = ensureRelativeDir(process.env.TMP_DIR ?? ".tmp", ".tmp", "TMP_DIR"); +/** 临时目录(相对项目根或绝对路径),默认 `.tmp` */ +export const RELATIVE_TMP_DIR = ensureConfigurableDir(process.env.TMP_DIR ?? ".tmp", ".tmp", "TMP_DIR"); /** 与 `media` 返回及静态路径一致,无前导 host */ export const POST_MEDIA_PUBLIC_PREFIX = `${STATIC_PUBLIC_PREFIX}/${MEDIA_UPLOAD_SUBDIR}/`; diff --git a/server/middleware/00.public.ts b/server/middleware/00.public.ts index c2003e2..7239660 100644 --- a/server/middleware/00.public.ts +++ b/server/middleware/00.public.ts @@ -1,4 +1,4 @@ -import { resolve, join, sep, extname } from "node:path"; +import { resolve, join, sep, extname, relative } from "node:path"; import { promises as fsp } from "node:fs"; import { decodePath, @@ -38,8 +38,9 @@ export default eventHandler(async (event: H3Event) => { const targetPath = join(SAFE_BASE_DIR, pathname); const resolvedPath = resolve(targetPath); - // 安全校验 - if (!resolvedPath.startsWith(SAFE_BASE_DIR + sep)) { + // 安全校验(支持 STATIC_DIR 为绝对路径;relative 在越界时为 `..` 开头) + const rel = relative(SAFE_BASE_DIR, resolvedPath); + if (rel.startsWith("..") || rel === "..") { res.statusCode = FORBIDDEN; return "Forbidden"; }