diff --git a/CHANGELOG.md b/CHANGELOG.md index d271b6756af..3683cf2ada0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -125,6 +125,7 @@ Docs: https://docs.openclaw.ai - Sandbox/Prompts: show the sandbox container workdir as the prompt working directory and clarify host-path usage for file tools, preventing host-path `exec` failures in sandbox sessions. (#16790) Thanks @carrotRakko. - Media/Security: allow local media reads from OpenClaw state `workspace/` and `sandboxes/` roots by default so generated workspace media can be delivered without unsafe global path bypasses. (#15541) Thanks @lanceji. - Media/Security: harden local media allowlist bypasses by requiring an explicit `readFile` override when callers mark paths as validated, and reject filesystem-root `localRoots` entries. (#16739) +- Media/Security: allow local media reads from default per-agent state workspaces (`workspace-`) so non-default agents can send workspace-local attachments. (#17136) Thanks @MisterGuy420. - Discord/Security: harden voice message media loading (SSRF + allowed-local-root checks) so tool-supplied paths/URLs cannot be used to probe internal URLs or read arbitrary local files. - Security/BlueBubbles: require explicit `mediaLocalRoots` allowlists for local outbound media path reads to prevent local file disclosure. (#16322) Thanks @mbelinky. - Security/BlueBubbles: reject ambiguous shared-path webhook routing when multiple webhook targets match the same guid/password. diff --git a/src/web/media.test.ts b/src/web/media.test.ts index 4a04d28cbbb..7ae28346295 100644 --- a/src/web/media.test.ts +++ b/src/web/media.test.ts @@ -1,3 +1,4 @@ +import fsSync, { type Dirent } from "node:fs"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; @@ -371,4 +372,38 @@ describe("local media root guard", () => { }), ); }); + + it("allows default OpenClaw state per-agent workspace-* roots", async () => { + const { STATE_DIR } = await import("../config/paths.js"); + const readFile = vi.fn(async () => Buffer.from("generated-media")); + const readdirSpy = vi.spyOn(fsSync, "readdirSync").mockReturnValue([ + { + name: "workspace-clawdy", + isDirectory: () => true, + } as unknown as Dirent, + { + name: "workspace-main", + isDirectory: () => true, + } as unknown as Dirent, + { + name: "workspace-file", + isDirectory: () => false, + } as unknown as Dirent, + ]); + + try { + await expect( + loadWebMedia(path.join(STATE_DIR, "workspace-clawdy", "tmp", "render.bin"), { + maxBytes: 1024 * 1024, + readFile, + }), + ).resolves.toEqual( + expect.objectContaining({ + kind: "unknown", + }), + ); + } finally { + readdirSpy.mockRestore(); + } + }); });