From 4f835c4c0df770a310b4a2c9150eb5ff4145dde4 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 21 Feb 2026 19:10:05 +0000 Subject: [PATCH] test(media): dedupe temp roots and cover directory attachment rejection --- .../media-understanding-misc.test.ts | 39 ++++++++++++++----- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/src/media-understanding/media-understanding-misc.test.ts b/src/media-understanding/media-understanding-misc.test.ts index 32e38577b..9279ce5e6 100644 --- a/src/media-understanding/media-understanding-misc.test.ts +++ b/src/media-understanding/media-understanding-misc.test.ts @@ -24,6 +24,15 @@ describe("media understanding scope", () => { const originalFetch = globalThis.fetch; +async function withTempRoot(prefix: string, run: (base: string) => Promise): Promise { + const base = await fs.mkdtemp(path.join(os.tmpdir(), prefix)); + try { + return await run(base); + } finally { + await fs.rm(base, { recursive: true, force: true }); + } +} + describe("media understanding attachments SSRF", () => { afterEach(() => { globalThis.fetch = originalFetch; @@ -44,8 +53,7 @@ describe("media understanding attachments SSRF", () => { }); it("reads local attachments inside configured roots", async () => { - const base = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-media-cache-allowed-")); - try { + await withTempRoot("openclaw-media-cache-allowed-", async (base) => { const allowedRoot = path.join(base, "allowed"); const attachmentPath = path.join(allowedRoot, "voice-note.m4a"); await fs.mkdir(allowedRoot, { recursive: true }); @@ -57,9 +65,7 @@ describe("media understanding attachments SSRF", () => { const result = await cache.getBuffer({ attachmentIndex: 0, maxBytes: 1024, timeoutMs: 1000 }); expect(result.buffer.toString()).toBe("ok"); - } finally { - await fs.rm(base, { recursive: true, force: true }); - } + }); }); it("blocks local attachments outside configured roots", async () => { @@ -75,12 +81,27 @@ describe("media understanding attachments SSRF", () => { ).rejects.toThrow(/has no path or URL/i); }); + it("blocks directory attachments even inside configured roots", async () => { + await withTempRoot("openclaw-media-cache-dir-", async (base) => { + const allowedRoot = path.join(base, "allowed"); + const attachmentPath = path.join(allowedRoot, "nested"); + await fs.mkdir(attachmentPath, { recursive: true }); + + const cache = new MediaAttachmentCache([{ index: 0, path: attachmentPath }], { + localPathRoots: [allowedRoot], + }); + + await expect( + cache.getBuffer({ attachmentIndex: 0, maxBytes: 1024, timeoutMs: 1000 }), + ).rejects.toThrow(/has no path or URL/i); + }); + }); + it("blocks symlink escapes that resolve outside configured roots", async () => { if (process.platform === "win32") { return; } - const base = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-media-cache-symlink-")); - try { + await withTempRoot("openclaw-media-cache-symlink-", async (base) => { const allowedRoot = path.join(base, "allowed"); const outsidePath = "/etc/passwd"; const symlinkPath = path.join(allowedRoot, "note.txt"); @@ -94,8 +115,6 @@ describe("media understanding attachments SSRF", () => { await expect( cache.getBuffer({ attachmentIndex: 0, maxBytes: 1024, timeoutMs: 1000 }), ).rejects.toThrow(/has no path or URL/i); - } finally { - await fs.rm(base, { recursive: true, force: true }); - } + }); }); });