From 5d6443799acc1e1983eb70d447170e44c79c7b5c Mon Sep 17 00:00:00 2001 From: Juan Camilo Date: Thu, 2 Apr 2026 16:14:35 +0200 Subject: [PATCH] fix: crypto.randomUUID for IDs, Azure Foundry detection, safety filter visibility Three targeted fixes: 1. Replace Math.random() with crypto.randomUUID() for message and tool call IDs in both openaiShim.ts and codexShim.ts. Math.random() is not cryptographically secure and predictable in seeded environments. 2. Anchor Azure endpoint detection to parsed hostname instead of raw URL regex. Adds support for Azure AI Foundry (services.ai.azure.com) alongside existing cognitiveservices and openai Azure endpoints. Prevents SSRF-style bypass via path segments. 3. Surface content safety filter blocks to the user. When Gemini or Azure returns finish_reason 'content_filter' or 'safety', emit a visible text block '[Content blocked by provider safety filter]' instead of silently returning empty/truncated content with stop_reason 'end_turn'. Applied to both streaming and non-streaming. --- src/services/api/codexShim.ts | 2 +- src/services/api/openaiShim.ts | 37 +++++++++++++++++++++++++++++++--- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/services/api/codexShim.ts b/src/services/api/codexShim.ts index 26ae237e..6cc51f5b 100644 --- a/src/services/api/codexShim.ts +++ b/src/services/api/codexShim.ts @@ -85,7 +85,7 @@ function makeUsage(usage?: { } function makeMessageId(): string { - return `msg_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}` + return `msg_${crypto.randomUUID().replace(/-/g, '')}` } function normalizeToolUseId(toolUseId: string | undefined): { diff --git a/src/services/api/openaiShim.ts b/src/services/api/openaiShim.ts index 2e767f1b..c8153eb4 100644 --- a/src/services/api/openaiShim.ts +++ b/src/services/api/openaiShim.ts @@ -231,7 +231,7 @@ function convertMessages( input?: unknown extra_content?: Record }) => ({ - id: tu.id ?? `call_${Math.random().toString(36).slice(2)}`, + id: tu.id ?? `call_${crypto.randomUUID().replace(/-/g, '')}`, type: 'function' as const, function: { name: tu.name ?? 'unknown', @@ -389,7 +389,7 @@ interface OpenAIStreamChunk { } function makeMessageId(): string { - return `msg_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}` + return `msg_${crypto.randomUUID().replace(/-/g, '')}` } function convertChunkUsage( @@ -610,6 +610,23 @@ async function* openaiStreamToAnthropic( : choice.finish_reason === 'length' ? 'max_tokens' : 'end_turn' + if (choice.finish_reason === 'content_filter' || choice.finish_reason === 'safety') { + // Gemini/Azure content safety filter blocked the response. + // Emit a visible text block so the user knows why output was truncated. + if (!hasEmittedContentStart) { + yield { + type: 'content_block_start', + index: contentBlockIndex, + content_block: { type: 'text', text: '' }, + } + hasEmittedContentStart = true + } + yield { + type: 'content_block_delta', + index: contentBlockIndex, + delta: { type: 'text_delta', text: '\n\n[Content blocked by provider safety filter]' }, + } + } lastStopReason = stopReason yield { @@ -841,7 +858,14 @@ class OpenAIShimMessages { } const apiKey = process.env.OPENAI_API_KEY ?? '' - const isAzure = /cognitiveservices\.azure\.com|openai\.azure\.com/.test(request.baseUrl) + // Detect Azure endpoints by hostname (not raw URL) to prevent bypass via + // path segments like https://evil.com/cognitiveservices.azure.com/ + let isAzure = false + try { + const { hostname } = new URL(request.baseUrl) + isAzure = hostname.endsWith('.azure.com') && + (hostname.includes('cognitiveservices') || hostname.includes('openai') || hostname.includes('services.ai')) + } catch { /* malformed URL — not Azure */ } if (apiKey) { if (isAzure) { @@ -1003,6 +1027,13 @@ class OpenAIShimMessages { ? 'max_tokens' : 'end_turn' + if (choice?.finish_reason === 'content_filter' || choice?.finish_reason === 'safety') { + content.push({ + type: 'text', + text: '\n\n[Content blocked by provider safety filter]', + }) + } + return { id: data.id ?? makeMessageId(), type: 'message',