From fc7f71def56cf7fe71387a518e01bf4739ff4106 Mon Sep 17 00:00:00 2001 From: Antigravity Assistant Date: Thu, 30 Apr 2026 02:54:49 -0300 Subject: [PATCH] fix: resolve security ReDoS in codex and bugs #1797 #1789 --- open-sse/executors/codex.ts | 25 ++++++++++++++--- open-sse/handlers/chatCore.ts | 53 ++++++++++++++++++++++++++++++++++- 2 files changed, 73 insertions(+), 5 deletions(-) diff --git a/open-sse/executors/codex.ts b/open-sse/executors/codex.ts index 49c200c8fb..cddc72c25a 100644 --- a/open-sse/executors/codex.ts +++ b/open-sse/executors/codex.ts @@ -619,10 +619,27 @@ function normalizeCodexTools(body: Record): void { } function getResponsesSubpath(endpointPath: unknown): string | null { - const normalizedEndpoint = String(endpointPath || "").replace(/\/+$/, ""); - const match = normalizedEndpoint.match(/(?:^|\/)responses(?:(\/.*))?$/i); - if (!match) return null; - return match[1] || ""; + let normalizedEndpoint = String(endpointPath || ""); + while (normalizedEndpoint.endsWith("/") && normalizedEndpoint.length > 0) { + normalizedEndpoint = normalizedEndpoint.slice(0, -1); + } + + const lower = normalizedEndpoint.toLowerCase(); + if (lower === "responses" || lower.endsWith("/responses")) { + return ""; + } + + const responsesSlash = "/responses/"; + const idx = lower.lastIndexOf(responsesSlash); + if (idx !== -1) { + return normalizedEndpoint.slice(idx + "/responses".length); + } + + if (lower.startsWith("responses/")) { + return normalizedEndpoint.slice("responses".length); + } + + return null; } export function isCompactResponsesEndpoint(endpointPath: unknown): boolean { diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index a131a7cbc6..7872b5cc73 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -1696,7 +1696,36 @@ export async function handleChatCore({ ) => { const preserveToolResultBlocks = options?.preserveToolResultBlocks === true; if (!Array.isArray(payload.messages)) return; - const messages = payload.messages as ClaudeMessage[]; + let messages = payload.messages as ClaudeMessage[]; + + // Extract system role messages (Issue #1797) + const systemMessages = messages.filter((m) => m.role === "system"); + if (systemMessages.length > 0) { + const extraBlocks: ClaudeContentBlock[] = []; + for (const sm of systemMessages) { + if (typeof sm.content === "string" && sm.content.length > 0) { + extraBlocks.push({ type: "text", text: sm.content }); + } else if (Array.isArray(sm.content)) { + for (const block of sm.content as ClaudeContentBlock[]) { + if (block?.type === "text" && typeof block.text === "string" && block.text.length > 0) { + extraBlocks.push(block); + } + } + } + } + if (extraBlocks.length > 0) { + const existingSystem = payload.system; + if (typeof existingSystem === "string" && existingSystem.length > 0) { + payload.system = [{ type: "text", text: existingSystem }, ...extraBlocks]; + } else if (Array.isArray(existingSystem)) { + payload.system = [...(existingSystem as ClaudeContentBlock[]), ...extraBlocks]; + } else { + payload.system = extraBlocks; + } + } + messages = messages.filter((m) => m.role !== "system"); + payload.messages = messages; + } // Anthropic rejects empty text blocks in native Messages payloads. for (const msg of messages) { @@ -1969,6 +1998,28 @@ export async function handleChatCore({ } translatedBody.model = finalModelToUpstream; + // #1789: Prevent output_config.effort from overriding effort encoded in model name (Codex) + if (provider === "codex" || provider?.startsWith("codex")) { + const hasEffortSuffix = finalModelToUpstream.match(/-(low|medium|high|xhigh)$/i); + if ( + hasEffortSuffix && + translatedBody.output_config && + typeof translatedBody.output_config === "object" + ) { + const oc = translatedBody.output_config as Record; + if (oc.effort) { + log?.warn?.( + "PARAMS", + `Stripped output_config.effort="${oc.effort}" because model "${finalModelToUpstream}" already encodes effort` + ); + delete oc.effort; + if (Object.keys(oc).length === 0) { + delete translatedBody.output_config; + } + } + } + } + // Strip unsupported parameters for reasoning models (o1, o3, etc.) const unsupported = getUnsupportedParams(provider, model); if (unsupported.length > 0) {