mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 05:51:24 +00:00
fix: harden file serving
This commit is contained in:
@@ -8,6 +8,7 @@ import type { Duplex } from "node:stream";
|
||||
import chokidar from "chokidar";
|
||||
import { type WebSocket, WebSocketServer } from "ws";
|
||||
import { isTruthyEnvValue } from "../infra/env.js";
|
||||
import { SafeOpenError, openFileWithinRoot } from "../infra/fs-safe.js";
|
||||
import { detectMime } from "../media/mime.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { ensureDir, resolveUserPath } from "../utils.js";
|
||||
@@ -145,30 +146,31 @@ async function resolveFilePath(rootReal: string, urlPath: string) {
|
||||
const rel = normalized.replace(/^\/+/, "");
|
||||
if (rel.split("/").some((p) => p === "..")) return null;
|
||||
|
||||
let candidate = path.join(rootReal, rel);
|
||||
const tryOpen = async (relative: string) => {
|
||||
try {
|
||||
return await openFileWithinRoot({ rootDir: rootReal, relativePath: relative });
|
||||
} catch (err) {
|
||||
if (err instanceof SafeOpenError) return null;
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
if (normalized.endsWith("/")) {
|
||||
candidate = path.join(candidate, "index.html");
|
||||
return await tryOpen(path.posix.join(rel, "index.html"));
|
||||
}
|
||||
|
||||
const candidate = path.join(rootReal, rel);
|
||||
try {
|
||||
const st = await fs.stat(candidate);
|
||||
const st = await fs.lstat(candidate);
|
||||
if (st.isSymbolicLink()) return null;
|
||||
if (st.isDirectory()) {
|
||||
candidate = path.join(candidate, "index.html");
|
||||
return await tryOpen(path.posix.join(rel, "index.html"));
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
const rootPrefix = rootReal.endsWith(path.sep) ? rootReal : `${rootReal}${path.sep}`;
|
||||
try {
|
||||
const lstat = await fs.lstat(candidate);
|
||||
if (lstat.isSymbolicLink()) return null;
|
||||
const real = await fs.realpath(candidate);
|
||||
if (!real.startsWith(rootPrefix)) return null;
|
||||
return real;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return await tryOpen(rel);
|
||||
}
|
||||
|
||||
function isDisabledByEnv() {
|
||||
@@ -311,8 +313,8 @@ export async function createCanvasHostHandler(
|
||||
return true;
|
||||
}
|
||||
|
||||
const filePath = await resolveFilePath(rootReal, urlPath);
|
||||
if (!filePath) {
|
||||
const opened = await resolveFilePath(rootReal, urlPath);
|
||||
if (!opened) {
|
||||
if (urlPath === "/" || urlPath.endsWith("/")) {
|
||||
res.statusCode = 404;
|
||||
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
||||
@@ -327,22 +329,30 @@ export async function createCanvasHostHandler(
|
||||
return true;
|
||||
}
|
||||
|
||||
const lower = filePath.toLowerCase();
|
||||
const { handle, realPath } = opened;
|
||||
let data: Buffer;
|
||||
try {
|
||||
data = await handle.readFile();
|
||||
} finally {
|
||||
await handle.close().catch(() => {});
|
||||
}
|
||||
|
||||
const lower = realPath.toLowerCase();
|
||||
const mime =
|
||||
lower.endsWith(".html") || lower.endsWith(".htm")
|
||||
? "text/html"
|
||||
: ((await detectMime({ filePath })) ?? "application/octet-stream");
|
||||
: ((await detectMime({ filePath: realPath })) ?? "application/octet-stream");
|
||||
|
||||
res.setHeader("Cache-Control", "no-store");
|
||||
if (mime === "text/html") {
|
||||
const html = await fs.readFile(filePath, "utf8");
|
||||
const html = data.toString("utf8");
|
||||
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
||||
res.end(liveReload ? injectCanvasLiveReload(html) : html);
|
||||
return true;
|
||||
}
|
||||
|
||||
res.setHeader("Content-Type", mime);
|
||||
res.end(await fs.readFile(filePath));
|
||||
res.end(data);
|
||||
return true;
|
||||
} catch (err) {
|
||||
opts.runtime.error(`canvasHost request failed: ${String(err)}`);
|
||||
|
||||
Reference in New Issue
Block a user