mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-05 21:32:53 +00:00
fix(sandbox): validate remote hardlink counts
This commit is contained in:
@@ -232,6 +232,56 @@ describe("remote sandbox fs bridge", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("does not reject malformed non-decimal hardlink counts", async () => {
|
||||
await withTempDir("openclaw-remote-fs-bridge-hardlink-", async (stateDir) => {
|
||||
const workspaceDir = path.join(stateDir, "workspace");
|
||||
await fs.mkdir(workspaceDir, { recursive: true });
|
||||
const runtime: RemoteShellSandboxHandle = {
|
||||
remoteWorkspaceDir: workspaceDir,
|
||||
remoteAgentWorkspaceDir: workspaceDir,
|
||||
runRemoteShellScript: async (command) => {
|
||||
if (command.script.includes('if [ -e "$1" ] || [ -L "$1" ]')) {
|
||||
return { stdout: Buffer.from("1\n"), stderr: Buffer.alloc(0), code: 0 };
|
||||
}
|
||||
if (command.script.includes('readlink -f -- "$cursor"')) {
|
||||
return {
|
||||
stdout: Buffer.from(`${workspaceDir}/note.txt\n`),
|
||||
stderr: Buffer.alloc(0),
|
||||
code: 0,
|
||||
};
|
||||
}
|
||||
if (command.script.includes('stat -c "%F|%h"')) {
|
||||
return {
|
||||
stdout: Buffer.from("regular file|0x2\n"),
|
||||
stderr: Buffer.alloc(0),
|
||||
code: 0,
|
||||
};
|
||||
}
|
||||
if (command.script.includes('stat -c "%F|%s|%y"')) {
|
||||
return {
|
||||
stdout: Buffer.from("regular file|12|2026-05-29 12:00:00.000000000 +0000\n"),
|
||||
stderr: Buffer.alloc(0),
|
||||
code: 0,
|
||||
};
|
||||
}
|
||||
throw new Error(`unexpected remote script: ${command.script}`);
|
||||
},
|
||||
};
|
||||
const bridge = createRemoteShellSandboxFsBridge({
|
||||
sandbox: createSandbox({
|
||||
workspaceDir,
|
||||
agentWorkspaceDir: workspaceDir,
|
||||
}),
|
||||
runtime,
|
||||
});
|
||||
|
||||
await expect(bridge.stat({ filePath: "note.txt" })).resolves.toMatchObject({
|
||||
type: "file",
|
||||
size: 12,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function withTempDir<T>(prefix: string, run: (stateDir: string) => Promise<T>): Promise<T> {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import path from "node:path";
|
||||
import { parseStrictNonNegativeInteger } from "../../infra/parse-finite-number.js";
|
||||
import { isPathInside } from "../../infra/path-guards.js";
|
||||
import type {
|
||||
SandboxBackendCommandParams,
|
||||
@@ -24,6 +25,14 @@ type ResolvedRemotePath = SandboxResolvedPath & {
|
||||
source: RemoteMountSource;
|
||||
};
|
||||
|
||||
function hasMultipleHardlinks(raw: string): boolean {
|
||||
const linkCount = parseStrictNonNegativeInteger(raw);
|
||||
if (linkCount !== undefined) {
|
||||
return linkCount > 1;
|
||||
}
|
||||
return /^\d+$/.test(raw);
|
||||
}
|
||||
|
||||
type MountInfo = {
|
||||
localRoot: string;
|
||||
containerRoot: string;
|
||||
@@ -513,7 +522,7 @@ class RemoteShellSandboxFsBridge implements SandboxFsBridge {
|
||||
return;
|
||||
}
|
||||
const [kind = "", linksRaw = "1"] = output.split("|");
|
||||
if (kind === "regular file" && Number(linksRaw) > 1) {
|
||||
if (kind === "regular file" && hasMultipleHardlinks(linksRaw)) {
|
||||
throw new Error(
|
||||
`Hardlinked path is not allowed under sandbox mount root: ${params.containerPath}`,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user