fix: execute sandboxed file ops inside containers (#4026)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 795ec6aa2f
Co-authored-by: davidbors-snyk <240482518+davidbors-snyk@users.noreply.github.com>
Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
Reviewed-by: @steipete
This commit is contained in:
davidbors-snyk
2026-02-13 17:29:10 +02:00
committed by GitHub
parent 1def8c5448
commit 29d7839582
20 changed files with 862 additions and 152 deletions

View File

@@ -28,6 +28,7 @@ type WebMediaOptions = {
ssrfPolicy?: SsrFPolicy;
/** Allowed root directories for local path reads. "any" skips the check (caller already validated). */
localRoots?: string[] | "any";
readFile?: (filePath: string) => Promise<Buffer>;
};
function getDefaultLocalRoots(): string[] {
@@ -165,7 +166,13 @@ async function loadWebMediaInternal(
mediaUrl: string,
options: WebMediaOptions = {},
): Promise<WebMediaResult> {
const { maxBytes, optimizeImages = true, ssrfPolicy, localRoots } = options;
const {
maxBytes,
optimizeImages = true,
ssrfPolicy,
localRoots,
readFile: readFileOverride,
} = options;
// Use fileURLToPath for proper handling of file:// URLs (handles file://localhost/path, etc.)
if (mediaUrl.startsWith("file://")) {
try {
@@ -267,7 +274,7 @@ async function loadWebMediaInternal(
await assertLocalMediaAllowed(mediaUrl, localRoots);
// Local path
const data = await fs.readFile(mediaUrl);
const data = readFileOverride ? await readFileOverride(mediaUrl) : await fs.readFile(mediaUrl);
const mime = await detectMime({ buffer: data, filePath: mediaUrl });
const kind = mediaKindFromMime(mime);
let fileName = path.basename(mediaUrl) || undefined;
@@ -287,27 +294,39 @@ async function loadWebMediaInternal(
export async function loadWebMedia(
mediaUrl: string,
maxBytes?: number,
maxBytesOrOptions?: number | WebMediaOptions,
options?: { ssrfPolicy?: SsrFPolicy; localRoots?: string[] | "any" },
): Promise<WebMediaResult> {
if (typeof maxBytesOrOptions === "number" || maxBytesOrOptions === undefined) {
return await loadWebMediaInternal(mediaUrl, {
maxBytes: maxBytesOrOptions,
optimizeImages: true,
ssrfPolicy: options?.ssrfPolicy,
localRoots: options?.localRoots,
});
}
return await loadWebMediaInternal(mediaUrl, {
maxBytes,
optimizeImages: true,
ssrfPolicy: options?.ssrfPolicy,
localRoots: options?.localRoots,
...maxBytesOrOptions,
optimizeImages: maxBytesOrOptions.optimizeImages ?? true,
});
}
export async function loadWebMediaRaw(
mediaUrl: string,
maxBytes?: number,
maxBytesOrOptions?: number | WebMediaOptions,
options?: { ssrfPolicy?: SsrFPolicy; localRoots?: string[] | "any" },
): Promise<WebMediaResult> {
if (typeof maxBytesOrOptions === "number" || maxBytesOrOptions === undefined) {
return await loadWebMediaInternal(mediaUrl, {
maxBytes: maxBytesOrOptions,
optimizeImages: false,
ssrfPolicy: options?.ssrfPolicy,
localRoots: options?.localRoots,
});
}
return await loadWebMediaInternal(mediaUrl, {
maxBytes,
...maxBytesOrOptions,
optimizeImages: false,
ssrfPolicy: options?.ssrfPolicy,
localRoots: options?.localRoots,
});
}