fix(media): allow os.tmpdir() paths in sandbox media source validation
resolveSandboxedMediaSource() rejected all paths outside the sandbox workspace root, including /tmp. This blocked sandboxed agents from sending locally-generated temp files (e.g. images from Python scripts) via messaging actions. Add an os.tmpdir() prefix check before the strict sandbox containment assertion, consistent with buildMediaLocalRoots() which already includes os.tmpdir() in its default allowlist. Path traversal through /tmp (e.g. /tmp/../etc/passwd) is prevented by path.resolve() normalization before the prefix check. Relates-to: #16382, #14174
This commit is contained in:
committed by
Peter Steinberger
parent
4cf5c3e109
commit
0bb81f7294
@@ -1,10 +1,54 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveSandboxedMediaSource } from "./sandbox-paths.js";
|
||||
|
||||
describe("resolveSandboxedMediaSource", () => {
|
||||
// Group 1: /tmp paths (the bug fix)
|
||||
it("allows absolute paths under os.tmpdir()", async () => {
|
||||
const sandboxDir = await fs.mkdtemp(path.join(os.tmpdir(), "sandbox-media-"));
|
||||
try {
|
||||
const result = await resolveSandboxedMediaSource({
|
||||
media: path.join(os.tmpdir(), "image.png"),
|
||||
sandboxRoot: sandboxDir,
|
||||
});
|
||||
expect(result).toBe(path.join(os.tmpdir(), "image.png"));
|
||||
} finally {
|
||||
await fs.rm(sandboxDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("allows file:// URLs pointing to os.tmpdir()", async () => {
|
||||
const sandboxDir = await fs.mkdtemp(path.join(os.tmpdir(), "sandbox-media-"));
|
||||
try {
|
||||
const tmpFile = path.join(os.tmpdir(), "photo.png");
|
||||
const fileUrl = pathToFileURL(tmpFile).href;
|
||||
const result = await resolveSandboxedMediaSource({
|
||||
media: fileUrl,
|
||||
sandboxRoot: sandboxDir,
|
||||
});
|
||||
expect(result).toBe(tmpFile);
|
||||
} finally {
|
||||
await fs.rm(sandboxDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("allows nested paths under os.tmpdir()", async () => {
|
||||
const sandboxDir = await fs.mkdtemp(path.join(os.tmpdir(), "sandbox-media-"));
|
||||
try {
|
||||
const result = await resolveSandboxedMediaSource({
|
||||
media: path.join(os.tmpdir(), "subdir", "deep", "file.png"),
|
||||
sandboxRoot: sandboxDir,
|
||||
});
|
||||
expect(result).toBe(path.join(os.tmpdir(), "subdir", "deep", "file.png"));
|
||||
} finally {
|
||||
await fs.rm(sandboxDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// Group 2: Sandbox-relative paths (existing behavior)
|
||||
it("resolves sandbox-relative paths", async () => {
|
||||
const sandboxDir = await fs.mkdtemp(path.join(os.tmpdir(), "sandbox-media-"));
|
||||
try {
|
||||
@@ -18,7 +62,8 @@ describe("resolveSandboxedMediaSource", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects paths outside sandbox root", async () => {
|
||||
// Group 3: Rejections (security)
|
||||
it("rejects paths outside sandbox root and tmpdir", async () => {
|
||||
const sandboxDir = await fs.mkdtemp(path.join(os.tmpdir(), "sandbox-media-"));
|
||||
try {
|
||||
await expect(
|
||||
@@ -71,6 +116,7 @@ describe("resolveSandboxedMediaSource", () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Group 4: Passthrough
|
||||
it("passes HTTP URLs through unchanged", async () => {
|
||||
const result = await resolveSandboxedMediaSource({
|
||||
media: "https://example.com/image.png",
|
||||
|
||||
@@ -90,12 +90,18 @@ export async function resolveSandboxedMediaSource(params: {
|
||||
throw new Error(`Invalid file:// URL for sandboxed media: ${raw}`);
|
||||
}
|
||||
}
|
||||
const resolved = await assertSandboxPath({
|
||||
// Allow files under os.tmpdir() — consistent with buildMediaLocalRoots() defaults.
|
||||
const resolved = path.resolve(params.sandboxRoot, candidate);
|
||||
const tmpDir = os.tmpdir();
|
||||
if (resolved === tmpDir || resolved.startsWith(tmpDir + path.sep)) {
|
||||
return resolved;
|
||||
}
|
||||
const sandboxResult = await assertSandboxPath({
|
||||
filePath: candidate,
|
||||
cwd: params.sandboxRoot,
|
||||
root: params.sandboxRoot,
|
||||
});
|
||||
return resolved.resolved;
|
||||
return sandboxResult.resolved;
|
||||
}
|
||||
|
||||
async function assertNoSymlinkEscape(
|
||||
|
||||
Reference in New Issue
Block a user