From 781775ec0875de4e37436ae4d1e78c9a75d0d8f0 Mon Sep 17 00:00:00 2001 From: Jacob Tomlinson Date: Tue, 31 Mar 2026 03:12:47 -0700 Subject: [PATCH] Media: secure image temp dirs (#58270) --- scripts/check-no-random-messaging-tmp.mjs | 5 +-- src/media/image-ops.tempdir.test.ts | 35 +++++++++++++++++++ src/media/image-ops.ts | 4 +-- .../check-no-random-messaging-tmp.test.ts | 9 ++++- 4 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 src/media/image-ops.tempdir.test.ts diff --git a/scripts/check-no-random-messaging-tmp.mjs b/scripts/check-no-random-messaging-tmp.mjs index e472082b5fc7..acabf81ec448 100644 --- a/scripts/check-no-random-messaging-tmp.mjs +++ b/scripts/check-no-random-messaging-tmp.mjs @@ -9,10 +9,11 @@ import { unwrapExpression, } from "./lib/ts-guard-utils.mjs"; -const sourceRoots = [ +export const messagingTmpdirGuardSourceRoots = [ "src/channels", "src/infra/outbound", "src/line", + "src/media", "src/media-understanding", "extensions", ]; @@ -72,7 +73,7 @@ export function findMessagingTmpdirCallLines(content, fileName = "source.ts") { export async function main() { await runCallsiteGuard({ importMetaUrl: import.meta.url, - sourceRoots, + sourceRoots: messagingTmpdirGuardSourceRoots, findCallLines: findMessagingTmpdirCallLines, skipRelativePath: (relativePath) => allowedRelativePaths.has(relativePath), header: "Found os.tmpdir()/tmpdir() usage in messaging/channel runtime sources:", diff --git a/src/media/image-ops.tempdir.test.ts b/src/media/image-ops.tempdir.test.ts new file mode 100644 index 000000000000..7d32c29d0370 --- /dev/null +++ b/src/media/image-ops.tempdir.test.ts @@ -0,0 +1,35 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js"; + +describe("image-ops temp dir", () => { + let createdTempDir = ""; + + beforeEach(() => { + process.env.OPENCLAW_IMAGE_BACKEND = "sips"; + const originalMkdtemp = fs.mkdtemp.bind(fs); + vi.spyOn(fs, "mkdtemp").mockImplementation(async (prefix) => { + createdTempDir = await originalMkdtemp(prefix); + return createdTempDir; + }); + }); + + afterEach(() => { + delete process.env.OPENCLAW_IMAGE_BACKEND; + vi.restoreAllMocks(); + vi.resetModules(); + }); + + it("creates sips temp dirs under the secured OpenClaw tmp root", async () => { + const { getImageMetadata } = await import("./image-ops.js"); + const secureRoot = resolvePreferredOpenClawTmpDir(); + + await getImageMetadata(Buffer.from("image")); + + expect(fs.mkdtemp).toHaveBeenCalledTimes(1); + expect(fs.mkdtemp).toHaveBeenCalledWith(path.join(secureRoot, "openclaw-img-")); + expect(createdTempDir.startsWith(path.join(secureRoot, "openclaw-img-"))).toBe(true); + await expect(fs.access(createdTempDir)).rejects.toMatchObject({ code: "ENOENT" }); + }); +}); diff --git a/src/media/image-ops.ts b/src/media/image-ops.ts index cbf0ed20ffc1..3403d25bcb20 100644 --- a/src/media/image-ops.ts +++ b/src/media/image-ops.ts @@ -1,6 +1,6 @@ import fs from "node:fs/promises"; -import os from "node:os"; import path from "node:path"; +import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js"; import { runExec } from "../process/exec.js"; type Sharp = typeof import("sharp"); @@ -134,7 +134,7 @@ function readJpegExifOrientation(buffer: Buffer): number | null { } async function withTempDir(fn: (dir: string) => Promise): Promise { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-img-")); + const dir = await fs.mkdtemp(path.join(resolvePreferredOpenClawTmpDir(), "openclaw-img-")); try { return await fn(dir); } finally { diff --git a/test/scripts/check-no-random-messaging-tmp.test.ts b/test/scripts/check-no-random-messaging-tmp.test.ts index 276a19962af3..9ecc9535eb9d 100644 --- a/test/scripts/check-no-random-messaging-tmp.test.ts +++ b/test/scripts/check-no-random-messaging-tmp.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it } from "vitest"; -import { findMessagingTmpdirCallLines } from "../../scripts/check-no-random-messaging-tmp.mjs"; +import { + findMessagingTmpdirCallLines, + messagingTmpdirGuardSourceRoots, +} from "../../scripts/check-no-random-messaging-tmp.mjs"; describe("check-no-random-messaging-tmp", () => { it("finds os.tmpdir calls imported from node:os", () => { @@ -41,4 +44,8 @@ describe("check-no-random-messaging-tmp", () => { `; expect(findMessagingTmpdirCallLines(source)).toEqual([]); }); + + it("guards src/media against host tmpdir usage", () => { + expect(messagingTmpdirGuardSourceRoots).toContain("src/media"); + }); });