fix(api): validate codex websocket bridge payload

This commit is contained in:
diegosouzapw
2026-04-25 12:39:21 -03:00
parent e766fb1f13
commit 7775c8cc50
4 changed files with 42 additions and 5 deletions

View File

@@ -45,6 +45,7 @@ export interface RegistryModel {
supportsVision?: boolean;
supportsXHighEffort?: boolean;
targetFormat?: string;
strip?: readonly string[];
unsupportedParams?: readonly string[];
/** Maximum context window in tokens */
contextLength?: number;

View File

@@ -323,7 +323,7 @@ function fixToolPairs(messages: Record<string, unknown>[]) {
if (Array.isArray(newMsg.tool_calls)) {
const filteredToolCalls = newMsg.tool_calls.filter(
(tc: any) => !tc.id || toolResultIds.has(tc.id)
(tc: Record<string, unknown>) => !tc.id || toolResultIds.has(tc.id)
);
if (filteredToolCalls.length !== newMsg.tool_calls.length) {
newMsg.tool_calls = filteredToolCalls;
@@ -333,7 +333,8 @@ function fixToolPairs(messages: Record<string, unknown>[]) {
if (Array.isArray(newMsg.content)) {
const filteredContent = newMsg.content.filter(
(block: any) => block.type !== "tool_use" || !block.id || toolResultIds.has(block.id)
(block: Record<string, unknown>) =>
block.type !== "tool_use" || !block.id || toolResultIds.has(block.id)
);
if (filteredContent.length !== newMsg.content.length) {
newMsg.content = filteredContent;
@@ -374,7 +375,7 @@ function fixToolPairs(messages: Record<string, unknown>[]) {
if (msg.role === "user" && Array.isArray(msg.content)) {
const filteredContent = msg.content.filter(
(block: any) =>
(block: Record<string, unknown>) =>
block.type !== "tool_result" || !block.tool_use_id || toolCallIds.has(block.tool_use_id)
);
if (filteredContent.length !== msg.content.length) {

View File

@@ -1,5 +1,6 @@
import { NextResponse } from "next/server";
import { createHash, timingSafeEqual } from "node:crypto";
import { z } from "zod";
import { CodexExecutor } from "@omniroute/open-sse/executors/codex.ts";
import { getApiKeyMetadata } from "@/lib/db/apiKeys";
import { authorizeWebSocketHandshake, extractWsTokenFromRequest } from "@/lib/ws/handshake";
@@ -12,6 +13,15 @@ const executor = new CodexExecutor();
type JsonRecord = Record<string, unknown>;
const bridgePayloadSchema = z
.object({
action: z.string().optional(),
requestUrl: z.string().optional(),
headers: z.record(z.string(), z.unknown()).optional(),
response: z.record(z.string(), z.unknown()).optional(),
})
.passthrough();
function isRecord(value: unknown): value is JsonRecord {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
@@ -179,8 +189,11 @@ export async function POST(request: Request) {
let body: JsonRecord;
try {
const parsed = await request.json();
body = isRecord(parsed) ? parsed : {};
const parsed = bridgePayloadSchema.safeParse(await request.json());
if (!parsed.success) {
return jsonError(400, "invalid_json", "Request body must be a JSON object");
}
body = parsed.data as JsonRecord;
} catch {
return jsonError(400, "invalid_json", "Request body must be JSON");
}

View File

@@ -403,3 +403,25 @@ test("Codex internal websocket bridge secret comparison handles mismatched lengt
assert.equal(bridgeSecretMatches("bridge-secret", "bridge-secret-extra"), false);
assert.equal(bridgeSecretMatches("bridge-secret", ""), false);
});
test("Codex internal websocket bridge rejects non-object JSON payloads", async () => {
await withEnv({ OMNIROUTE_WS_BRIDGE_SECRET: "bridge-secret" }, async () => {
const { POST } = await import("../../src/app/api/internal/codex-responses-ws/route.ts");
const response = await POST(
new Request("http://omniroute.local/api/internal/codex-responses-ws", {
method: "POST",
headers: {
"content-type": "application/json",
"x-omniroute-ws-bridge-secret": "bridge-secret",
},
body: JSON.stringify(["invalid"]),
})
);
const body = await response.json();
assert.equal(response.status, 400);
assert.equal(body.error.code, "invalid_json");
assert.match(body.error.message, /JSON object/);
});
});