fix: tighten sandbox mkdirp boundary checks (#30610) (thanks @glitch418x)

This commit is contained in:
Agent
2026-03-01 21:41:24 +00:00
committed by Peter Steinberger
parent 687f5779d1
commit 3be1343e00
6 changed files with 40 additions and 17 deletions

View File

@@ -32,7 +32,7 @@ export type OpenBoundaryFileSyncParams = {
rootRealPath?: string;
maxBytes?: number;
rejectHardlinks?: boolean;
allowedTypes?: readonly SafeOpenSyncAllowedType[];
allowedType?: SafeOpenSyncAllowedType;
skipLexicalRootCheck?: boolean;
ioFs?: BoundaryReadFs;
};
@@ -79,7 +79,7 @@ export function openBoundaryFileSync(params: OpenBoundaryFileSyncParams): Bounda
resolvedPath,
rejectHardlinks: params.rejectHardlinks ?? true,
maxBytes: params.maxBytes,
allowedTypes: params.allowedTypes,
allowedType: params.allowedType,
ioFs,
});
if (!opened.ok) {

View File

@@ -28,14 +28,14 @@ describe("openVerifiedFileSync", () => {
});
});
it("accepts directories when allowedTypes includes directory", async () => {
it("accepts directories when allowedType is directory", async () => {
await withTempDir("openclaw-safe-open-", async (root) => {
const targetDir = path.join(root, "nested");
await fsp.mkdir(targetDir, { recursive: true });
const opened = openVerifiedFileSync({
filePath: targetDir,
allowedTypes: ["directory"],
allowedType: "directory",
rejectHardlinks: true,
});
expect(opened.ok).toBe(true);

View File

@@ -30,11 +30,11 @@ export function openVerifiedFileSync(params: {
rejectPathSymlink?: boolean;
rejectHardlinks?: boolean;
maxBytes?: number;
allowedTypes?: readonly SafeOpenSyncAllowedType[];
allowedType?: SafeOpenSyncAllowedType;
ioFs?: SafeOpenSyncFs;
}): SafeOpenSyncResult {
const ioFs = params.ioFs ?? fs;
const allowedTypes = params.allowedTypes ?? ["file"];
const allowedType = params.allowedType ?? "file";
const openReadFlags =
ioFs.constants.O_RDONLY |
(typeof ioFs.constants.O_NOFOLLOW === "number" ? ioFs.constants.O_NOFOLLOW : 0);
@@ -49,7 +49,7 @@ export function openVerifiedFileSync(params: {
const realPath = params.resolvedPath ?? ioFs.realpathSync(params.filePath);
const preOpenStat = ioFs.lstatSync(realPath);
if (!isAllowedType(preOpenStat, allowedTypes)) {
if (!isAllowedType(preOpenStat, allowedType)) {
return { ok: false, reason: "validation" };
}
if (params.rejectHardlinks && preOpenStat.isFile() && preOpenStat.nlink > 1) {
@@ -65,7 +65,7 @@ export function openVerifiedFileSync(params: {
fd = ioFs.openSync(realPath, openReadFlags);
const openedStat = ioFs.fstatSync(fd);
if (!isAllowedType(openedStat, allowedTypes)) {
if (!isAllowedType(openedStat, allowedType)) {
return { ok: false, reason: "validation" };
}
if (params.rejectHardlinks && openedStat.isFile() && openedStat.nlink > 1) {
@@ -93,11 +93,9 @@ export function openVerifiedFileSync(params: {
}
}
function isAllowedType(stat: fs.Stats, allowedTypes: readonly SafeOpenSyncAllowedType[]): boolean {
return allowedTypes.some((allowedType) => {
if (allowedType === "file") {
return stat.isFile();
}
function isAllowedType(stat: fs.Stats, allowedType: SafeOpenSyncAllowedType): boolean {
if (allowedType === "directory") {
return stat.isDirectory();
});
}
return stat.isFile();
}