[codex] Extract filesystem safety primitives (#77918)

* refactor: extract filesystem safety primitives

* refactor: use fs-safe for file access helpers

* refactor: reuse fs-safe for media reads

* refactor: use fs-safe for image reads

* refactor: reuse fs-safe in qqbot media opener

* refactor: reuse fs-safe for local media checks

* refactor: consume cleaner fs-safe api

* refactor: align fs-safe json option names

* fix: preserve fs-safe migration contracts

* refactor: use fs-safe primitive subpaths

* refactor: use grouped fs-safe subpaths

* refactor: align fs-safe api usage

* refactor: adapt private state store api

* chore: refresh proof gate

* refactor: follow fs-safe json api split

* refactor: follow reduced fs-safe surface

* build: default fs-safe python helper off

* fix: preserve fs-safe plugin sdk aliases

* refactor: consolidate fs-safe usage

* refactor: unify fs-safe store usage

* refactor: trim fs-safe temp workspace usage

* refactor: hide low-level fs-safe primitives

* build: use published fs-safe package

* fix: preserve outbound recovery durability after rebase

* chore: refresh pr checks
This commit is contained in:
Peter Steinberger
2026-05-06 02:15:17 +01:00
committed by GitHub
parent 61481eb34f
commit 538605ff44
356 changed files with 4918 additions and 11913 deletions

View File

@@ -1,5 +1,6 @@
import crypto from "node:crypto";
import path from "node:path";
import { sanitizeUntrustedFileName } from "openclaw/plugin-sdk/security-runtime";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
@@ -35,9 +36,7 @@ const AUDIO_MIME_MP3 = new Set(["audio/mpeg", "audio/mp3"]);
const AUDIO_MIME_CAF = new Set(["audio/x-caf", "audio/caf"]);
function sanitizeFilename(input: string | undefined, fallback: string): string {
const trimmed = input?.trim() ?? "";
const base = trimmed ? path.basename(trimmed) : "";
const name = base || fallback;
const name = sanitizeUntrustedFileName(input ?? "", fallback);
// Strip characters that could enable multipart header injection (CWE-93)
return name.replace(/[\r\n"\\]/g, "_");
}

View File

@@ -1,13 +1,8 @@
import { constants as fsConstants } from "node:fs";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import {
basenameFromMediaSource,
safeFileURLToPath,
readLocalFileFromRoots,
} from "openclaw/plugin-sdk/file-access-runtime";
import { resolveChannelMediaMaxBytes } from "openclaw/plugin-sdk/media-runtime";
import { lowercasePreservingWhitespace } from "openclaw/plugin-sdk/text-runtime";
import { resolveBlueBubblesAccount } from "./accounts.js";
import { sendBlueBubblesAttachment } from "./attachments.js";
import { resolveBlueBubblesMessageId } from "./monitor-reply-cache.js";
@@ -31,61 +26,6 @@ function assertMediaWithinLimit(sizeBytes: number, maxBytes?: number): void {
throw new Error(`Media exceeds ${maxLabel}MB limit (got ${sizeLabel}MB)`);
}
function resolveLocalMediaPath(source: string): string {
if (!source.startsWith("file://")) {
return source;
}
try {
return safeFileURLToPath(source);
} catch {
throw new Error(`Invalid file:// URL: ${source}`);
}
}
function expandHomePath(input: string): string {
if (input === "~") {
return os.homedir();
}
if (input.startsWith("~/") || input.startsWith(`~${path.sep}`)) {
return path.join(os.homedir(), input.slice(2));
}
return input;
}
function resolveConfiguredPath(input: string): string {
const trimmed = input.trim();
if (!trimmed) {
throw new Error("Empty mediaLocalRoots entry is not allowed");
}
if (trimmed.startsWith("file://")) {
try {
return safeFileURLToPath(trimmed);
} catch {
throw new Error(`Invalid file:// URL in mediaLocalRoots: ${input}`);
}
}
const resolved = expandHomePath(trimmed);
if (!path.isAbsolute(resolved)) {
throw new Error(`mediaLocalRoots entries must be absolute paths: ${input}`);
}
return resolved;
}
function isPathInsideRoot(candidate: string, root: string): boolean {
const normalizedCandidate = path.normalize(candidate);
const normalizedRoot = path.normalize(root);
const rootWithSep = normalizedRoot.endsWith(path.sep)
? normalizedRoot
: normalizedRoot + path.sep;
if (process.platform === "win32") {
const candidateLower = lowercasePreservingWhitespace(normalizedCandidate);
const rootLower = lowercasePreservingWhitespace(normalizedRoot);
const rootWithSepLower = lowercasePreservingWhitespace(rootWithSep);
return candidateLower === rootLower || candidateLower.startsWith(rootWithSepLower);
}
return normalizedCandidate === normalizedRoot || normalizedCandidate.startsWith(rootWithSep);
}
function resolveMediaLocalRoots(params: { cfg: OpenClawConfig; accountId?: string }): string[] {
const account = resolveBlueBubblesAccount({
cfg: params.cfg,
@@ -111,60 +51,17 @@ async function assertLocalMediaPathAllowed(params: {
);
}
const resolvedLocalPath = path.resolve(params.localPath);
const supportsNoFollow = process.platform !== "win32" && "O_NOFOLLOW" in fsConstants;
const openFlags = fsConstants.O_RDONLY | (supportsNoFollow ? fsConstants.O_NOFOLLOW : 0);
for (const rootEntry of params.localRoots) {
const resolvedRootInput = resolveConfiguredPath(rootEntry);
const relativeToRoot = path.relative(resolvedRootInput, resolvedLocalPath);
if (
relativeToRoot.startsWith("..") ||
path.isAbsolute(relativeToRoot) ||
relativeToRoot === ""
) {
continue;
}
let rootReal: string;
try {
rootReal = await fs.realpath(resolvedRootInput);
} catch {
rootReal = path.resolve(resolvedRootInput);
}
const candidatePath = path.resolve(rootReal, relativeToRoot);
if (!isPathInsideRoot(candidatePath, rootReal)) {
continue;
}
let handle: Awaited<ReturnType<typeof fs.open>> | null = null;
try {
handle = await fs.open(candidatePath, openFlags);
const realPath = await fs.realpath(candidatePath);
if (!isPathInsideRoot(realPath, rootReal)) {
continue;
}
const stat = await handle.stat();
if (!stat.isFile()) {
continue;
}
const realStat = await fs.stat(realPath);
if (stat.ino !== realStat.ino || stat.dev !== realStat.dev) {
continue;
}
const data = await handle.readFile();
return { data, realPath, sizeBytes: stat.size };
} catch {
// Try next configured root.
continue;
} finally {
if (handle) {
await handle.close().catch(() => {});
}
}
const localFile = await readLocalFileFromRoots({
filePath: params.localPath,
roots: params.localRoots,
label: "mediaLocalRoots",
});
if (localFile) {
return {
data: localFile.buffer,
realPath: localFile.realPath,
sizeBytes: localFile.stat.size,
};
}
throw new Error(
@@ -244,9 +141,8 @@ export async function sendBlueBubblesMedia(params: {
resolvedContentType = resolvedContentType ?? fetched.contentType ?? undefined;
resolvedFilename = resolvedFilename ?? fetched.fileName;
} else {
const localPath = expandHomePath(resolveLocalMediaPath(source));
const localFile = await assertLocalMediaPathAllowed({
localPath,
localPath: source,
localRoots: mediaLocalRoots,
accountId,
});