






















@@ -28,6 +28,19 @@ function makeTmpProbePath(prefix: string): string {
2828return `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}.txt`;
2929}
303031+async function withManagedMediaRoot<T>(run: (ctx: { stateDir: string }) => Promise<T>) {
32+const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-managed-media-"));
33+vi.stubEnv("OPENCLAW_STATE_DIR", stateDir);
34+try {
35+await fs.mkdir(path.join(stateDir, "media", "outbound"), { recursive: true });
36+await fs.mkdir(path.join(stateDir, "media", "tool-image-generation"), { recursive: true });
37+return await run({ stateDir });
38+} finally {
39+vi.unstubAllEnvs();
40+await fs.rm(stateDir, { recursive: true, force: true });
41+}
42+}
43+3144async function withOutsideHardlinkInOpenClawTmp<T>(
3245params: {
3346openClawTmpDir: string;
@@ -99,6 +112,31 @@ describe("resolveSandboxedMediaSource", () => {
99112});
100113});
101114115+it.each([
116+{
117+name: "managed outbound media",
118+relative: path.join("media", "outbound", "reply.png"),
119+},
120+{
121+name: "managed tool media",
122+relative: path.join("media", "tool-image-generation", "generated.png"),
123+},
124+])("allows $name outside the sandbox root", async ({ relative }) => {
125+await withManagedMediaRoot(async ({ stateDir }) => {
126+await withSandboxRoot(async (sandboxDir) => {
127+const media = path.join(stateDir, relative);
128+await fs.writeFile(media, "image", "utf8");
129+130+const result = await resolveSandboxedMediaSource({
131+ media,
132+sandboxRoot: sandboxDir,
133+});
134+135+expect(result).toBe(media);
136+});
137+});
138+});
139+102140// Group 2: Sandbox-relative paths (existing behavior)
103141it("resolves sandbox-relative paths", async () => {
104142await withSandboxRoot(async (sandboxDir) => {
@@ -283,6 +321,28 @@ describe("resolveSandboxedMediaSource", () => {
283321);
284322});
285323324+it("rejects symlinked managed media paths escaping the managed media root", async () => {
325+if (process.platform === "win32") {
326+return;
327+}
328+await withManagedMediaRoot(async ({ stateDir }) => {
329+await withSandboxRoot(async (sandboxDir) => {
330+const outsideDir = await fs.mkdtemp(path.join(os.tmpdir(), "managed-media-outside-"));
331+const outsideFile = path.join(outsideDir, "secret.png");
332+const symlinkPath = path.join(stateDir, "media", "outbound", "linked-secret.png");
333+try {
334+await fs.writeFile(outsideFile, "secret", "utf8");
335+await fs.symlink(outsideFile, symlinkPath);
336+337+await expectSandboxRejection(symlinkPath, sandboxDir, /managed media root|symlink/i);
338+} finally {
339+await fs.rm(symlinkPath, { force: true });
340+await fs.rm(outsideDir, { recursive: true, force: true });
341+}
342+});
343+});
344+});
345+286346// Group 4: Passthrough
287347it("passes HTTP URLs through unchanged", async () => {
288348const result = await resolveSandboxedMediaSource({
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。