mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 11:28:38 +00:00
fix(sandbox): reject hardlinked tmp media aliases
This commit is contained in:
committed by
Peter Steinberger
parent
a01849e163
commit
22689b9dc9
@@ -150,6 +150,82 @@ describe("resolveSandboxedMediaSource", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("rejects hardlinked OpenClaw tmp paths to outside files", async () => {
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const outsideDir = await fs.mkdtemp(
|
||||||
|
path.join(process.cwd(), "sandbox-media-hardlink-outside-"),
|
||||||
|
);
|
||||||
|
const outsideFile = path.join(outsideDir, "outside-secret.txt");
|
||||||
|
const hardlinkPath = path.join(
|
||||||
|
openClawTmpDir,
|
||||||
|
`sandbox-media-hardlink-${Date.now()}-${Math.random().toString(16).slice(2)}.txt`,
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
if (isPathInside(openClawTmpDir, outsideFile)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await fs.writeFile(outsideFile, "secret", "utf8");
|
||||||
|
await fs.mkdir(openClawTmpDir, { recursive: true });
|
||||||
|
try {
|
||||||
|
await fs.link(outsideFile, hardlinkPath);
|
||||||
|
} catch (err) {
|
||||||
|
if ((err as NodeJS.ErrnoException).code === "EXDEV") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
await withSandboxRoot(async (sandboxDir) => {
|
||||||
|
await expectSandboxRejection(hardlinkPath, sandboxDir, /hard.?link|sandbox/i);
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await fs.rm(hardlinkPath, { force: true });
|
||||||
|
await fs.rm(outsideDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects symlinked OpenClaw tmp paths to hardlinked outside files", async () => {
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const outsideDir = await fs.mkdtemp(
|
||||||
|
path.join(process.cwd(), "sandbox-media-hardlink-outside-"),
|
||||||
|
);
|
||||||
|
const outsideFile = path.join(outsideDir, "outside-secret.txt");
|
||||||
|
const hardlinkPath = path.join(
|
||||||
|
openClawTmpDir,
|
||||||
|
`sandbox-media-hardlink-target-${Date.now()}-${Math.random().toString(16).slice(2)}.txt`,
|
||||||
|
);
|
||||||
|
const symlinkPath = path.join(
|
||||||
|
openClawTmpDir,
|
||||||
|
`sandbox-media-hardlink-symlink-${Date.now()}-${Math.random().toString(16).slice(2)}.txt`,
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
if (isPathInside(openClawTmpDir, outsideFile)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await fs.writeFile(outsideFile, "secret", "utf8");
|
||||||
|
await fs.mkdir(openClawTmpDir, { recursive: true });
|
||||||
|
try {
|
||||||
|
await fs.link(outsideFile, hardlinkPath);
|
||||||
|
} catch (err) {
|
||||||
|
if ((err as NodeJS.ErrnoException).code === "EXDEV") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
await fs.symlink(hardlinkPath, symlinkPath);
|
||||||
|
await withSandboxRoot(async (sandboxDir) => {
|
||||||
|
await expectSandboxRejection(symlinkPath, sandboxDir, /hard.?link|sandbox/i);
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await fs.rm(symlinkPath, { force: true });
|
||||||
|
await fs.rm(hardlinkPath, { force: true });
|
||||||
|
await fs.rm(outsideDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Group 4: Passthrough
|
// Group 4: Passthrough
|
||||||
it("passes HTTP URLs through unchanged", async () => {
|
it("passes HTTP URLs through unchanged", async () => {
|
||||||
const result = await resolveSandboxedMediaSource({
|
const result = await resolveSandboxedMediaSource({
|
||||||
|
|||||||
@@ -187,9 +187,30 @@ async function resolveAllowedTmpMediaPath(params: {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
await assertNoSymlinkEscape(path.relative(openClawTmpDir, resolved), openClawTmpDir);
|
await assertNoSymlinkEscape(path.relative(openClawTmpDir, resolved), openClawTmpDir);
|
||||||
|
await assertNoHardlinkedFinalPath(resolved, openClawTmpDir);
|
||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function assertNoHardlinkedFinalPath(filePath: string, root: string): Promise<void> {
|
||||||
|
let stat: Awaited<ReturnType<typeof fs.stat>>;
|
||||||
|
try {
|
||||||
|
stat = await fs.stat(filePath);
|
||||||
|
} catch (err) {
|
||||||
|
if (isNotFoundPathError(err)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
if (!stat.isFile()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (stat.nlink > 1) {
|
||||||
|
throw new Error(
|
||||||
|
`Hardlinked tmp media path is not allowed under sandbox root (${shortPath(root)}): ${shortPath(filePath)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function assertNoSymlinkEscape(
|
async function assertNoSymlinkEscape(
|
||||||
relative: string,
|
relative: string,
|
||||||
root: string,
|
root: string,
|
||||||
|
|||||||
Reference in New Issue
Block a user