import { readFile, realpath } from "node:fs/promises"; import { resolve, sep } from "node:path"; const LOCAL_UPLOAD_URL_PREFIX = "/uploads/local/"; function isLoopbackHostname(hostname: string) { const normalized = hostname.toLowerCase(); return normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1"; } export function resolveTrustedLocalImageCandidate( imageInput: string, workspaceRoot = process.cwd(), ) { const raw = String(imageInput || "").trim(); if (!raw) return null; let pathname = ""; if (raw.startsWith("/")) { pathname = new URL(raw, "http://localhost").pathname; } else { let url: URL; try { url = new URL(raw); } catch { return null; } if (!["http:", "https:"].includes(url.protocol) || !isLoopbackHostname(url.hostname)) { return null; } pathname = url.pathname; } let decodedPathname: string; try { decodedPathname = decodeURIComponent(pathname); } catch { return null; } if (!decodedPathname.startsWith(LOCAL_UPLOAD_URL_PREFIX)) return null; const uploadRoot = resolve(workspaceRoot, "public", "uploads", "local"); const relativePath = decodedPathname.slice(LOCAL_UPLOAD_URL_PREFIX.length); const candidatePath = resolve(uploadRoot, relativePath); if (!relativePath || !candidatePath.startsWith(`${uploadRoot}${sep}`)) return null; return { candidatePath, uploadRoot }; } export async function readTrustedLocalImage( imageInput: string, workspaceRoot = process.cwd(), ) { const candidate = resolveTrustedLocalImageCandidate(imageInput, workspaceRoot); if (!candidate) return null; const [realUploadRoot, realCandidatePath] = await Promise.all([ realpath(candidate.uploadRoot), realpath(candidate.candidatePath), ]); if (!realCandidatePath.startsWith(`${realUploadRoot}${sep}`)) { throw new Error("本地图片路径不允许访问"); } return readFile(realCandidatePath); }