mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 18:28:26 +00:00
refactor(media): unify safe local file reads
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { logVerbose, shouldLogVerbose } from "../globals.js";
|
||||
import type { SsrFPolicy } from "../infra/net/ssrf.js";
|
||||
import { logVerbose, shouldLogVerbose } from "../globals.js";
|
||||
import { SafeOpenError, readLocalFileSafely } from "../infra/fs-safe.js";
|
||||
import { type MediaKind, maxBytesForKind, mediaKindFromMime } from "../media/constants.js";
|
||||
import { fetchRemoteMedia } from "../media/fetch.js";
|
||||
import {
|
||||
@@ -33,6 +34,25 @@ type WebMediaOptions = {
|
||||
readFile?: (filePath: string) => Promise<Buffer>;
|
||||
};
|
||||
|
||||
export type LocalMediaAccessErrorCode =
|
||||
| "path-not-allowed"
|
||||
| "invalid-root"
|
||||
| "invalid-file-url"
|
||||
| "unsafe-bypass"
|
||||
| "not-found"
|
||||
| "invalid-path"
|
||||
| "not-file";
|
||||
|
||||
export class LocalMediaAccessError extends Error {
|
||||
code: LocalMediaAccessErrorCode;
|
||||
|
||||
constructor(code: LocalMediaAccessErrorCode, message: string, options?: ErrorOptions) {
|
||||
super(message, options);
|
||||
this.code = code;
|
||||
this.name = "LocalMediaAccessError";
|
||||
}
|
||||
}
|
||||
|
||||
export function getDefaultLocalRoots(): readonly string[] {
|
||||
return getDefaultMediaLocalRoots();
|
||||
}
|
||||
@@ -65,7 +85,10 @@ async function assertLocalMediaAllowed(
|
||||
if (rel && !rel.startsWith("..") && !path.isAbsolute(rel)) {
|
||||
const firstSegment = rel.split(path.sep)[0] ?? "";
|
||||
if (firstSegment.startsWith("workspace-")) {
|
||||
throw new Error(`Local media path is not under an allowed directory: ${mediaPath}`);
|
||||
throw new LocalMediaAccessError(
|
||||
"path-not-allowed",
|
||||
`Local media path is not under an allowed directory: ${mediaPath}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,7 +101,8 @@ async function assertLocalMediaAllowed(
|
||||
resolvedRoot = path.resolve(root);
|
||||
}
|
||||
if (resolvedRoot === path.parse(resolvedRoot).root) {
|
||||
throw new Error(
|
||||
throw new LocalMediaAccessError(
|
||||
"invalid-root",
|
||||
`Invalid localRoots entry (refuses filesystem root): ${root}. Pass a narrower directory.`,
|
||||
);
|
||||
}
|
||||
@@ -86,7 +110,10 @@ async function assertLocalMediaAllowed(
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new Error(`Local media path is not under an allowed directory: ${mediaPath}`);
|
||||
throw new LocalMediaAccessError(
|
||||
"path-not-allowed",
|
||||
`Local media path is not under an allowed directory: ${mediaPath}`,
|
||||
);
|
||||
}
|
||||
|
||||
const HEIC_MIME_RE = /^image\/hei[cf]$/i;
|
||||
@@ -202,7 +229,7 @@ async function loadWebMediaInternal(
|
||||
try {
|
||||
mediaUrl = fileURLToPath(mediaUrl);
|
||||
} catch {
|
||||
throw new Error(`Invalid file:// URL: ${mediaUrl}`);
|
||||
throw new LocalMediaAccessError("invalid-file-url", `Invalid file:// URL: ${mediaUrl}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,7 +322,8 @@ async function loadWebMediaInternal(
|
||||
}
|
||||
|
||||
if ((sandboxValidated || localRoots === "any") && !readFileOverride) {
|
||||
throw new Error(
|
||||
throw new LocalMediaAccessError(
|
||||
"unsafe-bypass",
|
||||
"Refusing localRoots bypass without readFile override. Use sandboxValidated with readFile, or pass explicit localRoots.",
|
||||
);
|
||||
}
|
||||
@@ -306,7 +334,35 @@ async function loadWebMediaInternal(
|
||||
}
|
||||
|
||||
// Local path
|
||||
const data = readFileOverride ? await readFileOverride(mediaUrl) : await fs.readFile(mediaUrl);
|
||||
let data: Buffer;
|
||||
if (readFileOverride) {
|
||||
data = await readFileOverride(mediaUrl);
|
||||
} else {
|
||||
try {
|
||||
data = (await readLocalFileSafely({ filePath: mediaUrl })).buffer;
|
||||
} catch (err) {
|
||||
if (err instanceof SafeOpenError) {
|
||||
if (err.code === "not-found") {
|
||||
throw new LocalMediaAccessError("not-found", `Local media file not found: ${mediaUrl}`, {
|
||||
cause: err,
|
||||
});
|
||||
}
|
||||
if (err.code === "not-file") {
|
||||
throw new LocalMediaAccessError(
|
||||
"not-file",
|
||||
`Local media path is not a file: ${mediaUrl}`,
|
||||
{ cause: err },
|
||||
);
|
||||
}
|
||||
throw new LocalMediaAccessError(
|
||||
"invalid-path",
|
||||
`Local media path is not safe to read: ${mediaUrl}`,
|
||||
{ cause: err },
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
const mime = await detectMime({ buffer: data, filePath: mediaUrl });
|
||||
const kind = mediaKindFromMime(mime);
|
||||
let fileName = path.basename(mediaUrl) || undefined;
|
||||
|
||||
Reference in New Issue
Block a user