refactor: extract tmp media resolver helper and dedupe sandbox-path tests

This commit is contained in:
Peter Steinberger
2026-02-22 00:45:39 +01:00
parent 8202582f4b
commit 55e38d3b44
2 changed files with 35 additions and 7 deletions

View File

@@ -18,6 +18,11 @@ async function expectSandboxRejection(media: string, sandboxRoot: string, patter
await expect(resolveSandboxedMediaSource({ media, sandboxRoot })).rejects.toThrow(pattern);
}
function isPathInside(root: string, target: string): boolean {
const relative = path.relative(path.resolve(root), path.resolve(target));
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
}
describe("resolveSandboxedMediaSource", () => {
// Group 1: /tmp paths (the bug fix)
it.each([
@@ -94,9 +99,15 @@ describe("resolveSandboxedMediaSource", () => {
if (process.platform === "win32") {
return;
}
const outsideTmpTarget = path.resolve(process.cwd(), "package.json");
if (isPathInside(os.tmpdir(), outsideTmpTarget)) {
return;
}
await withSandboxRoot(async (sandboxDir) => {
await fs.access(outsideTmpTarget);
const symlinkPath = path.join(sandboxDir, "tmp-link-escape");
await fs.symlink("/etc/passwd", symlinkPath);
await fs.symlink(outsideTmpTarget, symlinkPath);
await expectSandboxRejection(symlinkPath, sandboxDir, /symlink|sandbox/i);
});
});

View File

@@ -90,12 +90,12 @@ export async function resolveSandboxedMediaSource(params: {
throw new Error(`Invalid file:// URL for sandboxed media: ${raw}`);
}
}
const resolved = path.resolve(resolveSandboxInputPath(candidate, params.sandboxRoot));
const tmpDir = path.resolve(os.tmpdir());
const candidateIsAbsolute = path.isAbsolute(expandPath(candidate));
if (candidateIsAbsolute && isPathInside(tmpDir, resolved)) {
await assertNoSymlinkEscape(path.relative(tmpDir, resolved), tmpDir);
return resolved;
const tmpMediaPath = await resolveAllowedTmpMediaPath({
candidate,
sandboxRoot: params.sandboxRoot,
});
if (tmpMediaPath) {
return tmpMediaPath;
}
const sandboxResult = await assertSandboxPath({
filePath: candidate,
@@ -105,6 +105,23 @@ export async function resolveSandboxedMediaSource(params: {
return sandboxResult.resolved;
}
async function resolveAllowedTmpMediaPath(params: {
candidate: string;
sandboxRoot: string;
}): Promise<string | undefined> {
const candidateIsAbsolute = path.isAbsolute(expandPath(params.candidate));
if (!candidateIsAbsolute) {
return undefined;
}
const resolved = path.resolve(resolveSandboxInputPath(params.candidate, params.sandboxRoot));
const tmpDir = path.resolve(os.tmpdir());
if (!isPathInside(tmpDir, resolved)) {
return undefined;
}
await assertNoSymlinkEscape(path.relative(tmpDir, resolved), tmpDir);
return resolved;
}
async function assertNoSymlinkEscape(
relative: string,
root: string,