mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 01:40:21 +00:00
fix(security): harden browser trace/download temp path handling
This commit is contained in:
@@ -8,24 +8,54 @@ function fallbackTmp(uid = 501) {
|
||||
return path.join("/var/fallback", `openclaw-${uid}`);
|
||||
}
|
||||
|
||||
function nodeErrorWithCode(code: string) {
|
||||
const err = new Error(code) as Error & { code?: string };
|
||||
err.code = code;
|
||||
return err;
|
||||
}
|
||||
|
||||
function secureDirStat(uid = 501) {
|
||||
return {
|
||||
isDirectory: () => true,
|
||||
isSymbolicLink: () => false,
|
||||
uid,
|
||||
mode: 0o40700,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveWithMocks(params: {
|
||||
lstatSync: NonNullable<TmpDirOptions["lstatSync"]>;
|
||||
fallbackLstatSync?: NonNullable<TmpDirOptions["lstatSync"]>;
|
||||
accessSync?: NonNullable<TmpDirOptions["accessSync"]>;
|
||||
uid?: number;
|
||||
tmpdirPath?: string;
|
||||
}) {
|
||||
const uid = params.uid ?? 501;
|
||||
const fallbackPath = fallbackTmp(uid);
|
||||
const accessSync = params.accessSync ?? vi.fn();
|
||||
const wrappedLstatSync = vi.fn((target: string) => {
|
||||
if (target === POSIX_OPENCLAW_TMP_DIR) {
|
||||
return params.lstatSync(target);
|
||||
}
|
||||
if (target === fallbackPath) {
|
||||
if (params.fallbackLstatSync) {
|
||||
return params.fallbackLstatSync(target);
|
||||
}
|
||||
return secureDirStat(uid);
|
||||
}
|
||||
return secureDirStat(uid);
|
||||
}) as NonNullable<TmpDirOptions["lstatSync"]>;
|
||||
const mkdirSync = vi.fn();
|
||||
const getuid = vi.fn(() => params.uid ?? 501);
|
||||
const getuid = vi.fn(() => uid);
|
||||
const tmpdir = vi.fn(() => params.tmpdirPath ?? "/var/fallback");
|
||||
const resolved = resolvePreferredOpenClawTmpDir({
|
||||
accessSync,
|
||||
lstatSync: params.lstatSync,
|
||||
lstatSync: wrappedLstatSync,
|
||||
mkdirSync,
|
||||
getuid,
|
||||
tmpdir,
|
||||
});
|
||||
return { resolved, accessSync, lstatSync: params.lstatSync, mkdirSync, tmpdir };
|
||||
return { resolved, accessSync, lstatSync: wrappedLstatSync, mkdirSync, tmpdir };
|
||||
}
|
||||
|
||||
describe("resolvePreferredOpenClawTmpDir", () => {
|
||||
@@ -45,24 +75,12 @@ describe("resolvePreferredOpenClawTmpDir", () => {
|
||||
});
|
||||
|
||||
it("prefers /tmp/openclaw when it does not exist but /tmp is writable", () => {
|
||||
const lstatSyncMock = vi.fn<NonNullable<TmpDirOptions["lstatSync"]>>(() => {
|
||||
const err = new Error("missing") as Error & { code?: string };
|
||||
err.code = "ENOENT";
|
||||
throw err;
|
||||
});
|
||||
|
||||
// second lstat call (after mkdir) should succeed
|
||||
lstatSyncMock.mockImplementationOnce(() => {
|
||||
const err = new Error("missing") as Error & { code?: string };
|
||||
err.code = "ENOENT";
|
||||
throw err;
|
||||
});
|
||||
lstatSyncMock.mockImplementationOnce(() => ({
|
||||
isDirectory: () => true,
|
||||
isSymbolicLink: () => false,
|
||||
uid: 501,
|
||||
mode: 0o40700,
|
||||
}));
|
||||
const lstatSyncMock = vi
|
||||
.fn<NonNullable<TmpDirOptions["lstatSync"]>>()
|
||||
.mockImplementationOnce(() => {
|
||||
throw nodeErrorWithCode("ENOENT");
|
||||
})
|
||||
.mockImplementationOnce(() => secureDirStat(501));
|
||||
|
||||
const { resolved, accessSync, mkdirSync, tmpdir } = resolveWithMocks({
|
||||
lstatSync: lstatSyncMock,
|
||||
@@ -84,7 +102,7 @@ describe("resolvePreferredOpenClawTmpDir", () => {
|
||||
const { resolved, tmpdir } = resolveWithMocks({ lstatSync });
|
||||
|
||||
expect(resolved).toBe(fallbackTmp());
|
||||
expect(tmpdir).toHaveBeenCalledTimes(1);
|
||||
expect(tmpdir).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to os.tmpdir()/openclaw when /tmp is not writable", () => {
|
||||
@@ -94,9 +112,7 @@ describe("resolvePreferredOpenClawTmpDir", () => {
|
||||
}
|
||||
});
|
||||
const lstatSync = vi.fn(() => {
|
||||
const err = new Error("missing") as Error & { code?: string };
|
||||
err.code = "ENOENT";
|
||||
throw err;
|
||||
throw nodeErrorWithCode("ENOENT");
|
||||
});
|
||||
const { resolved, tmpdir } = resolveWithMocks({
|
||||
accessSync,
|
||||
@@ -104,7 +120,7 @@ describe("resolvePreferredOpenClawTmpDir", () => {
|
||||
});
|
||||
|
||||
expect(resolved).toBe(fallbackTmp());
|
||||
expect(tmpdir).toHaveBeenCalledTimes(1);
|
||||
expect(tmpdir).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back when /tmp/openclaw is a symlink", () => {
|
||||
@@ -118,7 +134,7 @@ describe("resolvePreferredOpenClawTmpDir", () => {
|
||||
const { resolved, tmpdir } = resolveWithMocks({ lstatSync });
|
||||
|
||||
expect(resolved).toBe(fallbackTmp());
|
||||
expect(tmpdir).toHaveBeenCalledTimes(1);
|
||||
expect(tmpdir).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back when /tmp/openclaw is not owned by the current user", () => {
|
||||
@@ -132,7 +148,7 @@ describe("resolvePreferredOpenClawTmpDir", () => {
|
||||
const { resolved, tmpdir } = resolveWithMocks({ lstatSync });
|
||||
|
||||
expect(resolved).toBe(fallbackTmp());
|
||||
expect(tmpdir).toHaveBeenCalledTimes(1);
|
||||
expect(tmpdir).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back when /tmp/openclaw is group/other writable", () => {
|
||||
@@ -145,6 +161,51 @@ describe("resolvePreferredOpenClawTmpDir", () => {
|
||||
const { resolved, tmpdir } = resolveWithMocks({ lstatSync });
|
||||
|
||||
expect(resolved).toBe(fallbackTmp());
|
||||
expect(tmpdir).toHaveBeenCalledTimes(1);
|
||||
expect(tmpdir).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("throws when fallback path is a symlink", () => {
|
||||
const lstatSync = vi.fn(() => ({
|
||||
isDirectory: () => true,
|
||||
isSymbolicLink: () => true,
|
||||
uid: 501,
|
||||
mode: 0o120777,
|
||||
}));
|
||||
const fallbackLstatSync = vi.fn(() => ({
|
||||
isDirectory: () => true,
|
||||
isSymbolicLink: () => true,
|
||||
uid: 501,
|
||||
mode: 0o120777,
|
||||
}));
|
||||
|
||||
expect(() =>
|
||||
resolveWithMocks({
|
||||
lstatSync,
|
||||
fallbackLstatSync,
|
||||
}),
|
||||
).toThrow(/Unsafe fallback OpenClaw temp dir/);
|
||||
});
|
||||
|
||||
it("creates fallback directory when missing, then validates ownership and mode", () => {
|
||||
const lstatSync = vi.fn(() => ({
|
||||
isDirectory: () => true,
|
||||
isSymbolicLink: () => true,
|
||||
uid: 501,
|
||||
mode: 0o120777,
|
||||
}));
|
||||
const fallbackLstatSync = vi
|
||||
.fn<NonNullable<TmpDirOptions["lstatSync"]>>()
|
||||
.mockImplementationOnce(() => {
|
||||
throw nodeErrorWithCode("ENOENT");
|
||||
})
|
||||
.mockImplementationOnce(() => secureDirStat(501));
|
||||
|
||||
const { resolved, mkdirSync } = resolveWithMocks({
|
||||
lstatSync,
|
||||
fallbackLstatSync,
|
||||
});
|
||||
|
||||
expect(resolved).toBe(fallbackTmp());
|
||||
expect(mkdirSync).toHaveBeenCalledWith(fallbackTmp(), { recursive: true, mode: 0o700 });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user