fix(security): harden archive extraction (#16203)

* fix(browser): confine upload paths for file chooser

* fix(browser): sanitize suggested download filenames

* chore(lint): avoid control regex in download sanitizer

* test(browser): cover absolute escape paths

* docs(browser): update upload example path

* refactor(browser): centralize upload path confinement

* fix(infra): harden tmp dir selection

* fix(security): harden archive extraction

* fix(infra): harden tar extraction filter
This commit is contained in:
Peter Steinberger
2026-02-14 14:42:08 +01:00
committed by GitHub
parent 9a134c8a10
commit 3aa94afcfd
19 changed files with 1179 additions and 100 deletions

View File

@@ -1,10 +1,23 @@
import type { Command } from "commander";
import { DEFAULT_UPLOAD_DIR, resolvePathsWithinRoot } from "../../browser/paths.js";
import { danger } from "../../globals.js";
import { defaultRuntime } from "../../runtime.js";
import { shortenHomePath } from "../../utils.js";
import { callBrowserRequest, type BrowserParentOpts } from "../browser-cli-shared.js";
import { resolveBrowserActionContext } from "./shared.js";
function normalizeUploadPaths(paths: string[]): string[] {
const result = resolvePathsWithinRoot({
rootDir: DEFAULT_UPLOAD_DIR,
requestedPaths: paths,
scopeLabel: `uploads directory (${DEFAULT_UPLOAD_DIR})`,
});
if (!result.ok) {
throw new Error(result.error);
}
return result.paths;
}
export function registerBrowserFilesAndDownloadsCommands(
browser: Command,
parentOpts: (cmd: Command) => BrowserParentOpts,
@@ -12,7 +25,10 @@ export function registerBrowserFilesAndDownloadsCommands(
browser
.command("upload")
.description("Arm file upload for the next file chooser")
.argument("<paths...>", "File paths to upload")
.argument(
"<paths...>",
"File paths to upload (must be within OpenClaw temp uploads dir, e.g. /tmp/openclaw/uploads/file.pdf)",
)
.option("--ref <ref>", "Ref id from snapshot to click after arming")
.option("--input-ref <ref>", "Ref id for <input type=file> to set directly")
.option("--element <selector>", "CSS selector for <input type=file>")
@@ -25,6 +41,7 @@ export function registerBrowserFilesAndDownloadsCommands(
.action(async (paths: string[], opts, cmd) => {
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
try {
const normalizedPaths = normalizeUploadPaths(paths);
const timeoutMs = Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : undefined;
const result = await callBrowserRequest<{ download: { path: string } }>(
parent,
@@ -33,7 +50,7 @@ export function registerBrowserFilesAndDownloadsCommands(
path: "/hooks/file-chooser",
query: profile ? { profile } : undefined,
body: {
paths,
paths: normalizedPaths,
ref: opts.ref?.trim() || undefined,
inputRef: opts.inputRef?.trim() || undefined,
element: opts.element?.trim() || undefined,