mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
fix(infra): avoid EISDIR leak to messaging when Read targets directory (Closes #31186)
This commit is contained in:
committed by
Peter Steinberger
parent
8a4d8c889c
commit
6398a0ba8f
@@ -73,6 +73,22 @@ describe("fs-safe", () => {
|
||||
).rejects.toMatchObject({ code: "outside-workspace" });
|
||||
});
|
||||
|
||||
it("rejects directory path within root without leaking EISDIR (issue #31186)", async () => {
|
||||
const root = await tempDirs.make("openclaw-fs-safe-root-");
|
||||
await fs.mkdir(path.join(root, "memory"), { recursive: true });
|
||||
|
||||
await expect(
|
||||
openFileWithinRoot({ rootDir: root, relativePath: "memory" }),
|
||||
).rejects.toMatchObject({ code: expect.stringMatching(/invalid-path|not-file/) });
|
||||
|
||||
const err = await openFileWithinRoot({
|
||||
rootDir: root,
|
||||
relativePath: "memory",
|
||||
}).catch((e: unknown) => e);
|
||||
expect(err).toBeInstanceOf(SafeOpenError);
|
||||
expect((err as SafeOpenError).message).not.toMatch(/EISDIR/i);
|
||||
});
|
||||
|
||||
it("reads a file within root", async () => {
|
||||
const root = await tempDirs.make("openclaw-fs-safe-root-");
|
||||
await fs.writeFile(path.join(root, "inside.txt"), "inside");
|
||||
|
||||
@@ -7,7 +7,12 @@ import path from "node:path";
|
||||
import { sameFileIdentity } from "./file-identity.js";
|
||||
import { expandHomePrefix } from "./home-dir.js";
|
||||
import { assertNoPathAliasEscape } from "./path-alias-guards.js";
|
||||
import { isNotFoundPathError, isPathInside, isSymlinkOpenError } from "./path-guards.js";
|
||||
import {
|
||||
hasNodeErrorCode,
|
||||
isNotFoundPathError,
|
||||
isPathInside,
|
||||
isSymlinkOpenError,
|
||||
} from "./path-guards.js";
|
||||
|
||||
export type SafeOpenErrorCode =
|
||||
| "invalid-path"
|
||||
@@ -68,6 +73,20 @@ async function openVerifiedLocalFile(
|
||||
rejectHardlinks?: boolean;
|
||||
},
|
||||
): Promise<SafeOpenResult> {
|
||||
// Reject directories before opening so we never surface EISDIR to callers (e.g. tool
|
||||
// results that get sent to messaging channels). See openclaw/openclaw#31186.
|
||||
try {
|
||||
const preStat = await fs.lstat(filePath);
|
||||
if (preStat.isDirectory()) {
|
||||
throw new SafeOpenError("not-file", "not a file");
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof SafeOpenError) {
|
||||
throw err;
|
||||
}
|
||||
// ENOENT and other lstat errors: fall through and let fs.open handle.
|
||||
}
|
||||
|
||||
let handle: FileHandle;
|
||||
try {
|
||||
handle = await fs.open(filePath, OPEN_READ_FLAGS);
|
||||
@@ -78,6 +97,10 @@ async function openVerifiedLocalFile(
|
||||
if (isSymlinkOpenError(err)) {
|
||||
throw new SafeOpenError("symlink", "symlink open blocked", { cause: err });
|
||||
}
|
||||
// Defensive: if open still throws EISDIR (e.g. race), sanitize so it never leaks.
|
||||
if (hasNodeErrorCode(err, "EISDIR")) {
|
||||
throw new SafeOpenError("not-file", "not a file");
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user