* 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
105 lines
2.9 KiB
TypeScript
105 lines
2.9 KiB
TypeScript
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
|
|
export const POSIX_OPENCLAW_TMP_DIR = "/tmp/openclaw";
|
|
|
|
type ResolvePreferredOpenClawTmpDirOptions = {
|
|
accessSync?: (path: string, mode?: number) => void;
|
|
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;
|
|
};
|
|
|
|
type MaybeNodeError = { code?: string };
|
|
|
|
function isNodeErrorWithCode(err: unknown, code: string): err is MaybeNodeError {
|
|
return (
|
|
typeof err === "object" &&
|
|
err !== null &&
|
|
"code" in err &&
|
|
(err as MaybeNodeError).code === code
|
|
);
|
|
}
|
|
|
|
export function resolvePreferredOpenClawTmpDir(
|
|
options: ResolvePreferredOpenClawTmpDirOptions = {},
|
|
): string {
|
|
const accessSync = options.accessSync ?? fs.accessSync;
|
|
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 = 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 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 fallback();
|
|
}
|
|
}
|