UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

277 lines (276 loc) 11.4 kB
import { n as isNotFoundPathError, r as isPathInside } from "./path-guards-Cp-mGr3-.js"; import { d as resolveConfigDir, m as shortenHomePath } from "./utils-P__uGsPB.js"; import { n as resolvePreferredOpenClawTmpDir } from "./tmp-openclaw-dir-DnyL0lW9.js"; import { t as isPassThroughRemoteMediaSource } from "./media-source-url-BL9SUd7E.js"; import { t as isWindowsDrivePath } from "./archive-path-B4B2gRTa.js"; import { a as safeFileURLToPath, r as hasEncodedFileUrlSeparator, t as assertNoWindowsNetworkPath } from "./local-file-access-B1HKoQAc.js"; import { n as assertNoPathAliasEscape } from "./path-alias-guards-BjQDysBY.js"; import fs from "node:fs"; import { URL } from "node:url"; import { promisify } from "node:util"; import path from "node:path"; import os from "node:os"; //#region src/agents/sandbox-paths.ts /** * Sandbox input path normalization and boundary checks. * * Handles host paths, file URLs, temporary media paths, and workspace root assertions. */ const DATA_URL_RE = /^data:/i; const SANDBOX_CONTAINER_WORKDIR = "/workspace"; const MANAGED_MEDIA_SUBDIRS = /* @__PURE__ */ new Set(["outbound"]); function normalizeAtPrefix(filePath) { return filePath.startsWith("@") ? filePath.slice(1) : filePath; } function expandPath(filePath) { const normalized = normalizeAtPrefix(filePath); if (normalized === "~") return os.homedir(); if (normalized.startsWith("~/")) return os.homedir() + normalized.slice(1); return normalized; } /** True when the path is absolute for the current platform or a Windows drive path (e.g. C:\\...), even if path.isAbsolute is false under POSIX rules. */ function hostPathLooksAbsolute(expanded) { return path.isAbsolute(expanded) || isWindowsDrivePath(expanded); } function resolveToCwd(filePath, cwd) { const expanded = expandPath(filePath); if (isWindowsDrivePath(expanded)) return path.win32.normalize(expanded); if (path.isAbsolute(expanded)) return expanded; return path.resolve(cwd, expanded); } function resolveSandboxInputPath(filePath, cwd) { return resolveToCwd(filePath, cwd); } function resolveSandboxPath(params) { const resolved = resolveSandboxInputPath(params.filePath, params.cwd); const rootResolved = path.resolve(params.root); const relative = path.relative(rootResolved, resolved); if (!relative || relative === "") return { resolved, relative: "" }; if (relative === ".." || relative.startsWith("../") || relative.startsWith("..\\") || path.isAbsolute(relative) || isWindowsDrivePath(relative)) throw new Error(`Path escapes sandbox root (${shortenHomePath(rootResolved)}): ${params.filePath}`); return { resolved, relative }; } const realpathNative = promisify(fs.realpath.native); async function resolveRawPathViaExistingAncestor(rawPath) { let cursor = rawPath; const missingSuffix = []; while (true) try { return path.resolve(await realpathNative(cursor), ...missingSuffix); } catch (error) { if (!isNotFoundPathError(error)) throw error; const parent = path.dirname(cursor); if (parent === cursor) throw error; missingSuffix.unshift(path.basename(cursor)); cursor = parent; } } async function assertRawParentWithinRoot(params) { if (process.platform === "win32") return { rootCanonical: path.resolve(params.root), targetCanonical: resolveSandboxInputPath(params.filePath, params.cwd) }; const expanded = expandPath(params.filePath); if (isWindowsDrivePath(expanded)) return { rootCanonical: path.resolve(params.root), targetCanonical: path.win32.normalize(expanded) }; const rawAbsolute = path.isAbsolute(expanded) ? expanded : `${params.cwd}${path.sep}${expanded}`; const hasTrailingSeparator = rawAbsolute.endsWith(path.sep); const rawParent = hasTrailingSeparator ? rawAbsolute : path.dirname(rawAbsolute); const finalSegment = hasTrailingSeparator ? "." : path.basename(rawAbsolute); const rootResolved = path.resolve(params.root); const { rootCanonical } = params; const parentCanonical = await resolveRawPathViaExistingAncestor(rawParent); const targetCanonical = path.resolve(rawAbsolute) === rootResolved ? await resolveRawPathViaExistingAncestor(rawAbsolute) : path.resolve(parentCanonical, finalSegment); if (targetCanonical !== rootCanonical && !isPathInside(rootCanonical, targetCanonical)) throw new Error(`Path escapes sandbox root (${shortenHomePath(rootCanonical)}): ${params.filePath}`); return { rootCanonical, targetCanonical }; } async function assertSandboxPath(params) { const root = path.resolve(params.root); const cwd = path.resolve(params.cwd); let rootCanonical = root; let resolutionCwd = cwd; let filePath = params.filePath; const expanded = expandPath(filePath); if (process.platform !== "win32" && !isWindowsDrivePath(expanded)) { const rootPromise = resolveRawPathViaExistingAncestor(root); const [canonicalRoot, canonicalCwd] = await Promise.all([rootPromise, cwd === root ? rootPromise : resolveRawPathViaExistingAncestor(cwd)]); rootCanonical = canonicalRoot; resolutionCwd = path.resolve(root, path.relative(rootCanonical, canonicalCwd)); const prefixes = [ [cwd, resolutionCwd], [root, root], [rootCanonical, root] ]; if (path.isAbsolute(expanded) && isPathInside(rootCanonical, canonicalCwd)) { const rootAlias = path.resolve(cwd, path.relative(canonicalCwd, rootCanonical)); if (!prefixes.some(([prefix]) => prefix === rootAlias) && await resolveRawPathViaExistingAncestor(rootAlias) === rootCanonical) prefixes.push([rootAlias, root]); } for (const [prefix, replacement] of prefixes.toSorted((a, b) => b[0].length - a[0].length)) { if (expanded === prefix) { filePath = replacement; break; } const prefixWithSeparator = prefix.endsWith(path.sep) ? prefix : `${prefix}${path.sep}`; if (expanded.startsWith(prefixWithSeparator)) { filePath = `${replacement}${replacement.endsWith(path.sep) ? "" : path.sep}${expanded.slice(prefixWithSeparator.length)}`; break; } } } const normalized = { filePath, cwd: resolutionCwd, root, rootCanonical }; const resolved = resolveSandboxPath(normalized); const policy = { allowFinalSymlinkForUnlink: params.allowFinalSymlinkForUnlink, allowFinalHardlinkForUnlink: params.allowFinalHardlinkForUnlink }; await assertNoPathAliasEscape({ absolutePath: resolved.resolved, rootPath: root, boundaryLabel: "sandbox root", policy }); const rawTarget = await assertRawParentWithinRoot(normalized); if (path.resolve(rawTarget.targetCanonical) !== path.resolve(resolved.resolved)) await assertNoPathAliasEscape({ absolutePath: rawTarget.targetCanonical, rootPath: rawTarget.rootCanonical, boundaryLabel: "sandbox root", policy }); return resolved; } function assertMediaNotDataUrl(media) { const raw = media.trim(); if (DATA_URL_RE.test(raw)) throw new Error("data: URLs are not supported for media. Use buffer instead."); } function resolveManagedMediaRoot(candidate) { const expanded = expandPath(candidate); if (!hostPathLooksAbsolute(expanded)) return; const mediaRoot = path.join(resolveConfigDir(), "media"); const resolvedMediaRoot = path.resolve(mediaRoot); const resolvedExpanded = path.resolve(expanded); if (resolvedExpanded === resolvedMediaRoot || !isPathInside(resolvedMediaRoot, resolvedExpanded)) return; const firstSegment = path.relative(resolvedMediaRoot, resolvedExpanded).split(path.sep)[0] ?? ""; return MANAGED_MEDIA_SUBDIRS.has(firstSegment) || firstSegment.startsWith("tool-") ? path.join(resolvedMediaRoot, firstSegment) : void 0; } async function resolveAllowedManagedMediaPath(candidate) { const expanded = expandPath(candidate); if (!resolveManagedMediaRoot(expanded)) return; const resolved = path.resolve(expanded); await assertNoManagedMediaAliasEscape({ filePath: resolved, managedMediaRoot: path.resolve(resolveConfigDir(), "media") }); return resolved; } async function resolveSandboxedMediaSource(params) { const raw = params.media.trim(); if (!raw) return raw; if (isPassThroughRemoteMediaSource(raw)) return raw; const containerWorkdir = path.posix.normalize((params.containerWorkdir ?? SANDBOX_CONTAINER_WORKDIR).replace(/\\/g, "/")).replace(/\/+$/, "") || "/"; let candidate = raw; if (/^file:/i.test(candidate)) { const workspaceMappedFromUrl = mapContainerWorkspaceFileUrl({ fileUrl: candidate, sandboxRoot: params.sandboxRoot, containerWorkdir }); if (workspaceMappedFromUrl) candidate = workspaceMappedFromUrl; else try { candidate = safeFileURLToPath(candidate); } catch (err) { throw new Error(`Invalid file:// URL for sandboxed media: ${err.message}`, { cause: err }); } } const containerWorkspaceMapped = mapContainerWorkspacePath({ candidate, sandboxRoot: params.sandboxRoot, containerWorkdir }); if (containerWorkspaceMapped) candidate = containerWorkspaceMapped; assertNoWindowsNetworkPath(candidate, "Sandbox media path"); const tmpMediaPath = await resolveAllowedTmpMediaPath({ candidate, sandboxRoot: params.sandboxRoot }); if (tmpMediaPath) return tmpMediaPath; const managedMediaPath = await resolveAllowedManagedMediaPath(candidate); if (managedMediaPath) return managedMediaPath; return (await assertSandboxPath({ filePath: candidate, cwd: params.sandboxRoot, root: params.sandboxRoot })).resolved; } async function assertNoManagedMediaAliasEscape(params) { await assertNoPathAliasEscape({ absolutePath: params.filePath, rootPath: params.managedMediaRoot, boundaryLabel: "managed media root" }); } function mapContainerWorkspaceFileUrl(params) { let parsed; try { parsed = new URL(params.fileUrl); } catch { return; } if (parsed.protocol !== "file:") return; const host = parsed.hostname.trim().toLowerCase(); if (host && host !== "localhost") return; if (hasEncodedFileUrlSeparator(parsed.pathname)) return; let normalizedPathname; try { normalizedPathname = decodeURIComponent(parsed.pathname).replace(/\\/g, "/"); } catch { return; } return mapContainerWorkspacePath({ candidate: normalizedPathname, sandboxRoot: params.sandboxRoot, containerWorkdir: params.containerWorkdir }); } function mapContainerWorkspacePath(params) { const normalized = params.candidate.replace(/\\/g, "/"); if (normalized === params.containerWorkdir) return path.resolve(params.sandboxRoot); const prefix = params.containerWorkdir === "/" ? "/" : `${params.containerWorkdir}/`; if (!normalized.startsWith(prefix)) return; const rel = normalized.slice(prefix.length); if (!rel) return path.resolve(params.sandboxRoot); return path.resolve(params.sandboxRoot, ...rel.split("/").filter(Boolean)); } async function resolveAllowedTmpMediaPath(params) { if (!hostPathLooksAbsolute(expandPath(params.candidate))) return; const resolved = path.resolve(resolveSandboxInputPath(params.candidate, params.sandboxRoot)); const openClawTmpDir = path.resolve(resolvePreferredOpenClawTmpDir()); if (!isPathInside(openClawTmpDir, resolved)) return; await assertNoTmpAliasEscape({ filePath: resolved, tmpRoot: openClawTmpDir }); return resolved; } async function assertNoTmpAliasEscape(params) { await assertNoPathAliasEscape({ absolutePath: params.filePath, rootPath: params.tmpRoot, boundaryLabel: "tmp root" }); } //#endregion export { resolveSandboxInputPath as a, resolveManagedMediaRoot as i, assertSandboxPath as n, resolveSandboxPath as o, resolveAllowedManagedMediaPath as r, resolveSandboxedMediaSource as s, assertMediaNotDataUrl as t };