fix: guard remote media fetches with SSRF checks

This commit is contained in:
Peter Steinberger
2026-02-02 04:04:27 -08:00
parent d842b28a15
commit 81c68f582d
11 changed files with 422 additions and 241 deletions

View File

@@ -1,6 +1,7 @@
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import type { SsrFPolicy } from "../infra/net/ssrf.js";
import { logVerbose, shouldLogVerbose } from "../globals.js";
import { type MediaKind, maxBytesForKind, mediaKindFromMime } from "../media/constants.js";
import { fetchRemoteMedia } from "../media/fetch.js";
@@ -23,6 +24,7 @@ export type WebMediaResult = {
type WebMediaOptions = {
maxBytes?: number;
optimizeImages?: boolean;
ssrfPolicy?: SsrFPolicy;
};
const HEIC_MIME_RE = /^image\/hei[cf]$/i;
@@ -122,7 +124,7 @@ async function loadWebMediaInternal(
mediaUrl: string,
options: WebMediaOptions = {},
): Promise<WebMediaResult> {
const { maxBytes, optimizeImages = true } = options;
const { maxBytes, optimizeImages = true, ssrfPolicy } = options;
// Use fileURLToPath for proper handling of file:// URLs (handles file://localhost/path, etc.)
if (mediaUrl.startsWith("file://")) {
try {
@@ -209,7 +211,7 @@ async function loadWebMediaInternal(
: optimizeImages
? Math.max(maxBytes, defaultFetchCap)
: maxBytes;
const fetched = await fetchRemoteMedia({ url: mediaUrl, maxBytes: fetchCap });
const fetched = await fetchRemoteMedia({ url: mediaUrl, maxBytes: fetchCap, ssrfPolicy });
const { buffer, contentType, fileName } = fetched;
const kind = mediaKindFromMime(contentType);
return await clampAndFinalize({ buffer, contentType, kind, fileName });
@@ -239,20 +241,27 @@ async function loadWebMediaInternal(
});
}
export async function loadWebMedia(mediaUrl: string, maxBytes?: number): Promise<WebMediaResult> {
export async function loadWebMedia(
mediaUrl: string,
maxBytes?: number,
options?: { ssrfPolicy?: SsrFPolicy },
): Promise<WebMediaResult> {
return await loadWebMediaInternal(mediaUrl, {
maxBytes,
optimizeImages: true,
ssrfPolicy: options?.ssrfPolicy,
});
}
export async function loadWebMediaRaw(
mediaUrl: string,
maxBytes?: number,
options?: { ssrfPolicy?: SsrFPolicy },
): Promise<WebMediaResult> {
return await loadWebMediaInternal(mediaUrl, {
maxBytes,
optimizeImages: false,
ssrfPolicy: options?.ssrfPolicy,
});
}