fix(security): harden browser trace/download temp path handling

This commit is contained in:
Peter Steinberger
2026-02-26 01:01:17 +01:00
parent e56b0cf1a0
commit 496a76c03b
8 changed files with 322 additions and 40 deletions

View File

@@ -1,6 +1,7 @@
import fs from "node:fs/promises";
import path from "node:path";
import { SafeOpenError, openFileWithinRoot } from "../infra/fs-safe.js";
import { isNotFoundPathError, isPathInside } from "../infra/path-guards.js";
import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js";
export const DEFAULT_BROWSER_TMP_DIR = resolvePreferredOpenClawTmpDir();
@@ -30,6 +31,67 @@ export function resolvePathWithinRoot(params: {
return { ok: true, path: resolved };
}
export async function resolveWritablePathWithinRoot(params: {
rootDir: string;
requestedPath: string;
scopeLabel: string;
defaultFileName?: string;
}): Promise<{ ok: true; path: string } | { ok: false; error: string }> {
const lexical = resolvePathWithinRoot(params);
if (!lexical.ok) {
return lexical;
}
const invalid = (): { ok: false; error: string } => ({
ok: false,
error: `Invalid path: must stay within ${params.scopeLabel}`,
});
const rootDir = path.resolve(params.rootDir);
let rootRealPath: string;
try {
const rootLstat = await fs.lstat(rootDir);
if (!rootLstat.isDirectory() || rootLstat.isSymbolicLink()) {
return invalid();
}
rootRealPath = await fs.realpath(rootDir);
} catch {
return invalid();
}
const requestedPath = lexical.path;
const parentDir = path.dirname(requestedPath);
try {
const parentLstat = await fs.lstat(parentDir);
if (!parentLstat.isDirectory() || parentLstat.isSymbolicLink()) {
return invalid();
}
const parentRealPath = await fs.realpath(parentDir);
if (!isPathInside(rootRealPath, parentRealPath)) {
return invalid();
}
} catch {
return invalid();
}
try {
const targetLstat = await fs.lstat(requestedPath);
if (targetLstat.isSymbolicLink() || !targetLstat.isFile()) {
return invalid();
}
const targetRealPath = await fs.realpath(requestedPath);
if (!isPathInside(rootRealPath, targetRealPath)) {
return invalid();
}
} catch (err) {
if (!isNotFoundPathError(err)) {
return invalid();
}
}
return lexical;
}
export function resolvePathsWithinRoot(params: {
rootDir: string;
requestedPaths: string[];