From bee771cbcdfddcbabb98fe3c56182068e87762fb Mon Sep 17 00:00:00 2001 From: tuntun1337 Date: Tue, 19 May 2026 11:57:58 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=9D=99=E6=80=81=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E8=B7=AF=E7=94=B1=E7=9A=84=E8=B7=AF=E5=BE=84=E7=A9=BF?= =?UTF-8?q?=E8=B6=8A=E6=BC=8F=E6=B4=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /static/js|css|img|fonts/:file 处理器把 URL 解码后的文件名直接拼进 join(basePath, ...)。Oak 会将 ..%2F..%2F..%2Fetc%2Fpasswd 解码为 ../../../etc/passwd,导致可读取静态目录之外的任意文件。 新增 resolveStaticPath():对解析后的真实路径做规范化,并校验其仍位于 目标静态子目录内(采用路径边界校验而非黑名单匹配),越界或不存在统一 返回 404。 --- src/utils/render.ts | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/utils/render.ts b/src/utils/render.ts index d4e993a..23710b3 100644 --- a/src/utils/render.ts +++ b/src/utils/render.ts @@ -1,33 +1,46 @@ /** * 模板渲染 / 读取工具 */ -import {join} from "https://deno.land/std/path/mod.ts"; +import {join, normalize, resolve, relative, isAbsolute} from "https://deno.land/std/path/mod.ts"; import {basePath} from "../config/constants.ts"; import {getMime} from "../utils/types.ts"; import {cyrb53_str} from "./common.ts"; +// 解析静态资源真实路径并校验是否越出静态目录,防止路径穿越 +function resolveStaticPath(subDir: string, pathname: string): string { + const baseDir = resolve(join(basePath, subDir)); + const target = resolve(baseDir, normalize(pathname)); + const rel = relative(baseDir, target); + if (rel === ".." || rel.startsWith(".." + "/") || rel.startsWith(".." + "\\") || isAbsolute(rel)) { + throw new Deno.errors.NotFound("Invalid static path"); + } + return target; +} + export async function getJS(ctx, pathname, status = 200): Promise { try { - ctx.response.body = await Deno.readTextFile(join(basePath, `/static/js/${pathname}`)); + ctx.response.body = await Deno.readTextFile(resolveStaticPath("/static/js", pathname)); ctx.response.status = status; ctx.response.headers.set("Content-Type", "application/javascript"); // ctx.response.headers.set("Cache-Control", "public, max-age=86400, immutable"); const hash = cyrb53_str(`${pathname}-${ctx.response.body.length}`); ctx.state.metadata = {etag: hash}; } catch (error) { + ctx.response.status = 404; } } export async function getCSS(ctx, pathname, status = 200): Promise { try { - ctx.response.body = await Deno.readTextFile(join(basePath, `/static/css/${pathname}`)); + ctx.response.body = await Deno.readTextFile(resolveStaticPath("/static/css", pathname)); ctx.response.status = status; ctx.response.headers.set("Content-Type", "text/css"); // ctx.response.headers.set("Cache-Control", "public, max-age=86400, immutable"); const hash = cyrb53_str(`${pathname}-${ctx.response.body.length}`); ctx.state.metadata = {etag: hash}; } catch (error) { + ctx.response.status = 404; } } @@ -35,7 +48,7 @@ export async function getIMG(ctx, pathname, status = 200): Promise { try { const extension = pathname.split('.').pop()?.toLowerCase() || ''; const contentType = getMime(extension) || 'application/octet-stream'; - ctx.response.body = await Deno.readFile(join(basePath, `/static/img/${pathname}`)); + ctx.response.body = await Deno.readFile(resolveStaticPath("/static/img", pathname)); ctx.response.status = status; ctx.response.headers.set("Content-Type", contentType); @@ -43,6 +56,7 @@ export async function getIMG(ctx, pathname, status = 200): Promise { const hash = cyrb53_str(`${pathname}-${ctx.response.body.length}`); ctx.state.metadata = {etag: hash}; } catch (error) { + ctx.response.status = 404; } } @@ -50,7 +64,7 @@ export async function getFONTS(ctx, pathname, status = 200): Promise { try { const extension = pathname.split('.').pop()?.toLowerCase() || ''; const contentType = getMime(extension) || 'application/octet-stream'; - ctx.response.body = await Deno.readFile(join(basePath, `/static/css/fonts/${pathname}`)); + ctx.response.body = await Deno.readFile(resolveStaticPath("/static/css/fonts", pathname)); ctx.response.status = status; ctx.response.headers.set("Content-Type", contentType); @@ -58,6 +72,7 @@ export async function getFONTS(ctx, pathname, status = 200): Promise { const hash = cyrb53_str(`${pathname}-${ctx.response.body.length}`); ctx.state.metadata = {etag: hash}; } catch (error) { + ctx.response.status = 404; } }