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

@@ -6,7 +6,14 @@ export const POSIX_OPENCLAW_TMP_DIR = "/tmp/openclaw";
type ResolvePreferredOpenClawTmpDirOptions = {
accessSync?: (path: string, mode?: number) => void;
statSync?: (path: string) => { isDirectory(): boolean };
lstatSync?: (path: string) => {
isDirectory(): boolean;
isSymbolicLink(): boolean;
mode?: number;
uid?: number;
};
mkdirSync?: (path: string, opts: { recursive: boolean; mode?: number }) => void;
getuid?: () => number | undefined;
tmpdir?: () => string;
};
@@ -25,26 +32,73 @@ export function resolvePreferredOpenClawTmpDir(
options: ResolvePreferredOpenClawTmpDirOptions = {},
): string {
const accessSync = options.accessSync ?? fs.accessSync;
const statSync = options.statSync ?? fs.statSync;
const lstatSync = options.lstatSync ?? fs.lstatSync;
const mkdirSync = options.mkdirSync ?? fs.mkdirSync;
const getuid =
options.getuid ??
(() => {
try {
return typeof process.getuid === "function" ? process.getuid() : undefined;
} catch {
return undefined;
}
});
const tmpdir = options.tmpdir ?? os.tmpdir;
const uid = getuid();
const isSecureDirForUser = (st: { mode?: number; uid?: number }): boolean => {
if (uid === undefined) {
return true;
}
if (typeof st.uid === "number" && st.uid !== uid) {
return false;
}
// Avoid group/other writable dirs when running on multi-user hosts.
if (typeof st.mode === "number" && (st.mode & 0o022) !== 0) {
return false;
}
return true;
};
const fallback = (): string => {
const base = tmpdir();
const suffix = uid === undefined ? "openclaw" : `openclaw-${uid}`;
return path.join(base, suffix);
};
try {
const preferred = statSync(POSIX_OPENCLAW_TMP_DIR);
if (!preferred.isDirectory()) {
return path.join(tmpdir(), "openclaw");
const preferred = lstatSync(POSIX_OPENCLAW_TMP_DIR);
if (!preferred.isDirectory() || preferred.isSymbolicLink()) {
return fallback();
}
accessSync(POSIX_OPENCLAW_TMP_DIR, fs.constants.W_OK | fs.constants.X_OK);
if (!isSecureDirForUser(preferred)) {
return fallback();
}
return POSIX_OPENCLAW_TMP_DIR;
} catch (err) {
if (!isNodeErrorWithCode(err, "ENOENT")) {
return path.join(tmpdir(), "openclaw");
return fallback();
}
}
try {
accessSync("/tmp", fs.constants.W_OK | fs.constants.X_OK);
// Create with a safe default; subsequent callers expect it exists.
mkdirSync(POSIX_OPENCLAW_TMP_DIR, { recursive: true, mode: 0o700 });
try {
const preferred = lstatSync(POSIX_OPENCLAW_TMP_DIR);
if (!preferred.isDirectory() || preferred.isSymbolicLink()) {
return fallback();
}
if (!isSecureDirForUser(preferred)) {
return fallback();
}
} catch {
return fallback();
}
return POSIX_OPENCLAW_TMP_DIR;
} catch {
return path.join(tmpdir(), "openclaw");
return fallback();
}
}