mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 02:38:26 +00:00
refactor(browser): split act route modules and dedupe path guards
This commit is contained in:
@@ -9,6 +9,58 @@ export const DEFAULT_TRACE_DIR = DEFAULT_BROWSER_TMP_DIR;
|
||||
export const DEFAULT_DOWNLOAD_DIR = path.join(DEFAULT_BROWSER_TMP_DIR, "downloads");
|
||||
export const DEFAULT_UPLOAD_DIR = path.join(DEFAULT_BROWSER_TMP_DIR, "uploads");
|
||||
|
||||
type InvalidPathResult = { ok: false; error: string };
|
||||
|
||||
function invalidPath(scopeLabel: string): InvalidPathResult {
|
||||
return {
|
||||
ok: false,
|
||||
error: `Invalid path: must stay within ${scopeLabel}`,
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveRealPathIfExists(targetPath: string): Promise<string | undefined> {
|
||||
try {
|
||||
return await fs.realpath(targetPath);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveTrustedRootRealPath(rootDir: string): Promise<string | undefined> {
|
||||
try {
|
||||
const rootLstat = await fs.lstat(rootDir);
|
||||
if (!rootLstat.isDirectory() || rootLstat.isSymbolicLink()) {
|
||||
return undefined;
|
||||
}
|
||||
return await fs.realpath(rootDir);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function validateCanonicalPathWithinRoot(params: {
|
||||
rootRealPath: string;
|
||||
candidatePath: string;
|
||||
expect: "directory" | "file";
|
||||
}): Promise<"ok" | "not-found" | "invalid"> {
|
||||
try {
|
||||
const candidateLstat = await fs.lstat(params.candidatePath);
|
||||
if (candidateLstat.isSymbolicLink()) {
|
||||
return "invalid";
|
||||
}
|
||||
if (params.expect === "directory" && !candidateLstat.isDirectory()) {
|
||||
return "invalid";
|
||||
}
|
||||
if (params.expect === "file" && !candidateLstat.isFile()) {
|
||||
return "invalid";
|
||||
}
|
||||
const candidateRealPath = await fs.realpath(params.candidatePath);
|
||||
return isPathInside(params.rootRealPath, candidateRealPath) ? "ok" : "invalid";
|
||||
} catch (err) {
|
||||
return isNotFoundPathError(err) ? "not-found" : "invalid";
|
||||
}
|
||||
}
|
||||
|
||||
export function resolvePathWithinRoot(params: {
|
||||
rootDir: string;
|
||||
requestedPath: string;
|
||||
@@ -42,51 +94,30 @@ export async function resolveWritablePathWithinRoot(params: {
|
||||
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 rootRealPath = await resolveTrustedRootRealPath(rootDir);
|
||||
if (!rootRealPath) {
|
||||
return invalidPath(params.scopeLabel);
|
||||
}
|
||||
|
||||
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();
|
||||
const parentStatus = await validateCanonicalPathWithinRoot({
|
||||
rootRealPath,
|
||||
candidatePath: parentDir,
|
||||
expect: "directory",
|
||||
});
|
||||
if (parentStatus !== "ok") {
|
||||
return invalidPath(params.scopeLabel);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
const targetStatus = await validateCanonicalPathWithinRoot({
|
||||
rootRealPath,
|
||||
candidatePath: requestedPath,
|
||||
expect: "file",
|
||||
});
|
||||
if (targetStatus === "invalid") {
|
||||
return invalidPath(params.scopeLabel);
|
||||
}
|
||||
|
||||
return lexical;
|
||||
@@ -141,13 +172,8 @@ async function resolveCheckedPathsWithinRoot(params: {
|
||||
allowMissingFallback: boolean;
|
||||
}): Promise<{ ok: true; paths: string[] } | { ok: false; error: string }> {
|
||||
const rootDir = path.resolve(params.rootDir);
|
||||
let rootRealPath: string | undefined;
|
||||
try {
|
||||
rootRealPath = await fs.realpath(rootDir);
|
||||
} catch {
|
||||
// Keep historical behavior for missing roots and rely on openFileWithinRoot for final checks.
|
||||
rootRealPath = undefined;
|
||||
}
|
||||
// Keep historical behavior for missing roots and rely on openFileWithinRoot for final checks.
|
||||
const rootRealPath = await resolveRealPathIfExists(rootDir);
|
||||
|
||||
const isInRoot = (relativePath: string) =>
|
||||
Boolean(relativePath) && !relativePath.startsWith("..") && !path.isAbsolute(relativePath);
|
||||
|
||||
Reference in New Issue
Block a user