mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 00:33:31 +00:00
refactor(security): centralize path guard helpers
This commit is contained in:
@@ -3,6 +3,7 @@ import { constants as fsConstants } from "node:fs";
|
||||
import type { FileHandle } from "node:fs/promises";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { isNotFoundPathError, isPathInside, isSymlinkOpenError } from "./path-guards.js";
|
||||
|
||||
export type SafeOpenErrorCode =
|
||||
| "invalid-path"
|
||||
@@ -34,27 +35,17 @@ export type SafeLocalReadResult = {
|
||||
stat: Stats;
|
||||
};
|
||||
|
||||
const NOT_FOUND_CODES = new Set(["ENOENT", "ENOTDIR"]);
|
||||
const SUPPORTS_NOFOLLOW = process.platform !== "win32" && "O_NOFOLLOW" in fsConstants;
|
||||
const OPEN_READ_FLAGS = fsConstants.O_RDONLY | (SUPPORTS_NOFOLLOW ? fsConstants.O_NOFOLLOW : 0);
|
||||
|
||||
const ensureTrailingSep = (value: string) => (value.endsWith(path.sep) ? value : value + path.sep);
|
||||
|
||||
const isNodeError = (err: unknown): err is NodeJS.ErrnoException =>
|
||||
Boolean(err && typeof err === "object" && "code" in (err as Record<string, unknown>));
|
||||
|
||||
const isNotFoundError = (err: unknown) =>
|
||||
isNodeError(err) && typeof err.code === "string" && NOT_FOUND_CODES.has(err.code);
|
||||
|
||||
const isSymlinkOpenError = (err: unknown) =>
|
||||
isNodeError(err) && (err.code === "ELOOP" || err.code === "EINVAL" || err.code === "ENOTSUP");
|
||||
|
||||
async function openVerifiedLocalFile(filePath: string): Promise<SafeOpenResult> {
|
||||
let handle: FileHandle;
|
||||
try {
|
||||
handle = await fs.open(filePath, OPEN_READ_FLAGS);
|
||||
} catch (err) {
|
||||
if (isNotFoundError(err)) {
|
||||
if (isNotFoundPathError(err)) {
|
||||
throw new SafeOpenError("not-found", "file not found");
|
||||
}
|
||||
if (isSymlinkOpenError(err)) {
|
||||
@@ -87,7 +78,7 @@ async function openVerifiedLocalFile(filePath: string): Promise<SafeOpenResult>
|
||||
if (err instanceof SafeOpenError) {
|
||||
throw err;
|
||||
}
|
||||
if (isNotFoundError(err)) {
|
||||
if (isNotFoundPathError(err)) {
|
||||
throw new SafeOpenError("not-found", "file not found");
|
||||
}
|
||||
throw err;
|
||||
@@ -102,14 +93,14 @@ export async function openFileWithinRoot(params: {
|
||||
try {
|
||||
rootReal = await fs.realpath(params.rootDir);
|
||||
} catch (err) {
|
||||
if (isNotFoundError(err)) {
|
||||
if (isNotFoundPathError(err)) {
|
||||
throw new SafeOpenError("not-found", "root dir not found");
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
const rootWithSep = ensureTrailingSep(rootReal);
|
||||
const resolved = path.resolve(rootWithSep, params.relativePath);
|
||||
if (!resolved.startsWith(rootWithSep)) {
|
||||
if (!isPathInside(rootWithSep, resolved)) {
|
||||
throw new SafeOpenError("invalid-path", "path escapes root");
|
||||
}
|
||||
|
||||
@@ -128,7 +119,7 @@ export async function openFileWithinRoot(params: {
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (!opened.realPath.startsWith(rootWithSep)) {
|
||||
if (!isPathInside(rootWithSep, opened.realPath)) {
|
||||
await opened.handle.close().catch(() => {});
|
||||
throw new SafeOpenError("invalid-path", "path escapes root");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user