fix: harden OpenResponses URL input fetching

This commit is contained in:
Peter Steinberger
2026-02-13 01:38:15 +01:00
parent 4199f9889f
commit 99f28031e5
13 changed files with 431 additions and 11 deletions

View File

@@ -63,6 +63,7 @@ type OpenResponsesHttpOptions = {
};
const DEFAULT_BODY_BYTES = 20 * 1024 * 1024;
const DEFAULT_MAX_URL_PARTS = 8;
function writeSseEvent(res: ServerResponse, event: StreamingEvent) {
res.write(`event: ${event.type}\n`);
@@ -89,10 +90,19 @@ function extractTextContent(content: string | ContentPart[]): string {
type ResolvedResponsesLimits = {
maxBodyBytes: number;
maxUrlParts: number;
files: InputFileLimits;
images: InputImageLimits;
};
function normalizeHostnameAllowlist(values: string[] | undefined): string[] | undefined {
if (!values || values.length === 0) {
return undefined;
}
const normalized = values.map((value) => value.trim()).filter((value) => value.length > 0);
return normalized.length > 0 ? normalized : undefined;
}
function resolveResponsesLimits(
config: GatewayHttpResponsesConfig | undefined,
): ResolvedResponsesLimits {
@@ -100,8 +110,13 @@ function resolveResponsesLimits(
const images = config?.images;
return {
maxBodyBytes: config?.maxBodyBytes ?? DEFAULT_BODY_BYTES,
maxUrlParts:
typeof config?.maxUrlParts === "number"
? Math.max(0, Math.floor(config.maxUrlParts))
: DEFAULT_MAX_URL_PARTS,
files: {
allowUrl: files?.allowUrl ?? true,
urlAllowlist: normalizeHostnameAllowlist(files?.urlAllowlist),
allowedMimes: normalizeMimeList(files?.allowedMimes, DEFAULT_INPUT_FILE_MIMES),
maxBytes: files?.maxBytes ?? DEFAULT_INPUT_FILE_MAX_BYTES,
maxChars: files?.maxChars ?? DEFAULT_INPUT_FILE_MAX_CHARS,
@@ -115,6 +130,7 @@ function resolveResponsesLimits(
},
images: {
allowUrl: images?.allowUrl ?? true,
urlAllowlist: normalizeHostnameAllowlist(images?.urlAllowlist),
allowedMimes: normalizeMimeList(images?.allowedMimes, DEFAULT_INPUT_IMAGE_MIMES),
maxBytes: images?.maxBytes ?? DEFAULT_INPUT_IMAGE_MAX_BYTES,
maxRedirects: images?.maxRedirects ?? DEFAULT_INPUT_MAX_REDIRECTS,
@@ -384,6 +400,15 @@ export async function handleOpenResponsesHttpRequest(
// Extract images + files from input (Phase 2)
let images: ImageContent[] = [];
let fileContexts: string[] = [];
let urlParts = 0;
const markUrlPart = () => {
urlParts += 1;
if (urlParts > limits.maxUrlParts) {
throw new Error(
`Too many URL-based input sources: ${urlParts} (limit: ${limits.maxUrlParts})`,
);
}
};
try {
if (Array.isArray(payload.input)) {
for (const item of payload.input) {
@@ -401,6 +426,9 @@ export async function handleOpenResponsesHttpRequest(
if (!sourceType) {
throw new Error("input_image must have 'source.url' or 'source.data'");
}
if (sourceType === "url") {
markUrlPart();
}
const imageSource: InputImageSource = {
type: sourceType,
url: source.url,
@@ -425,6 +453,9 @@ export async function handleOpenResponsesHttpRequest(
if (!sourceType) {
throw new Error("input_file must have 'source.url' or 'source.data'");
}
if (sourceType === "url") {
markUrlPart();
}
const file = await extractFileContentFromSource({
source: {
type: sourceType,