mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-13 12:10:34 +00:00
fix(security): unify root-bound write hardening
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
readPathWithinRoot,
|
||||
readLocalFileSafely,
|
||||
writeFileWithinRoot,
|
||||
writeFileFromPathWithinRoot,
|
||||
} from "./fs-safe.js";
|
||||
|
||||
const tempDirs = createTrackedTempDirs();
|
||||
@@ -213,6 +214,20 @@ describe("fs-safe", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("writes a file within root from another local source path safely", async () => {
|
||||
const root = await tempDirs.make("openclaw-fs-safe-root-");
|
||||
const outside = await tempDirs.make("openclaw-fs-safe-src-");
|
||||
const sourcePath = path.join(outside, "source.bin");
|
||||
await fs.writeFile(sourcePath, "hello-from-source");
|
||||
await writeFileFromPathWithinRoot({
|
||||
rootDir: root,
|
||||
relativePath: "nested/from-source.txt",
|
||||
sourcePath,
|
||||
});
|
||||
await expect(fs.readFile(path.join(root, "nested", "from-source.txt"), "utf8")).resolves.toBe(
|
||||
"hello-from-source",
|
||||
);
|
||||
});
|
||||
it("rejects write traversal outside root", async () => {
|
||||
const root = await tempDirs.make("openclaw-fs-safe-root-");
|
||||
await expect(
|
||||
@@ -295,6 +310,49 @@ describe("fs-safe", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it.runIf(process.platform !== "win32")(
|
||||
"does not clobber out-of-root file when symlink retarget races write-from-path open",
|
||||
async () => {
|
||||
const root = await tempDirs.make("openclaw-fs-safe-root-");
|
||||
const inside = path.join(root, "inside");
|
||||
const outside = await tempDirs.make("openclaw-fs-safe-outside-");
|
||||
const sourceDir = await tempDirs.make("openclaw-fs-safe-source-");
|
||||
const sourcePath = path.join(sourceDir, "source.txt");
|
||||
await fs.writeFile(sourcePath, "new-content");
|
||||
await fs.mkdir(inside, { recursive: true });
|
||||
const outsideTarget = path.join(outside, "target.txt");
|
||||
await fs.writeFile(outsideTarget, "X".repeat(4096));
|
||||
const slot = path.join(root, "slot");
|
||||
await fs.symlink(inside, slot);
|
||||
|
||||
const realRealpath = fs.realpath.bind(fs);
|
||||
let flipped = false;
|
||||
const realpathSpy = vi.spyOn(fs, "realpath").mockImplementation(async (...args) => {
|
||||
const [filePath] = args;
|
||||
if (!flipped && String(filePath).endsWith(path.join("slot", "target.txt"))) {
|
||||
flipped = true;
|
||||
await fs.rm(slot, { recursive: true, force: true });
|
||||
await fs.symlink(outside, slot);
|
||||
}
|
||||
return await realRealpath(...args);
|
||||
});
|
||||
try {
|
||||
await expect(
|
||||
writeFileFromPathWithinRoot({
|
||||
rootDir: root,
|
||||
relativePath: path.join("slot", "target.txt"),
|
||||
sourcePath,
|
||||
mkdir: false,
|
||||
}),
|
||||
).rejects.toMatchObject({ code: "outside-workspace" });
|
||||
} finally {
|
||||
realpathSpy.mockRestore();
|
||||
}
|
||||
|
||||
await expect(fs.readFile(outsideTarget, "utf8")).resolves.toBe("X".repeat(4096));
|
||||
},
|
||||
);
|
||||
|
||||
it.runIf(process.platform !== "win32")(
|
||||
"cleans up created out-of-root file when symlink retarget races create path",
|
||||
async () => {
|
||||
|
||||
Reference in New Issue
Block a user