Fase 8 · Bloco D — injection-guard em todas as rotas LLM + red-team (#3857)

Integrated into release/v3.8.25 — Fase 8 Bloco D (injection-guard em todas as rotas LLM + red-team).
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-14 18:02:18 -03:00
committed by GitHub
parent d3146a1751
commit d728bfbb1e
19 changed files with 260 additions and 12 deletions

View File

@@ -1,4 +1,5 @@
import { handleAudioSpeech } from "@omniroute/open-sse/handlers/audioSpeech.ts";
import { withInjectionGuard } from "@/middleware/promptInjectionGuard";
import { getProviderCredentials, clearRecoveredProviderState } from "@/sse/services/auth";
import {
parseSpeechModel,
@@ -33,7 +34,7 @@ export async function OPTIONS() {
* POST /v1/audio/speech — text-to-speech
* OpenAI TTS API compatible. Returns audio stream.
*/
export async function POST(request) {
async function postHandler(request, context) {
let rawBody;
try {
rawBody = await request.json();
@@ -110,3 +111,5 @@ export async function POST(request) {
}
return response;
}
export const POST = withInjectionGuard(postHandler);

View File

@@ -13,6 +13,7 @@ import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
import { getAllCustomModels, getApiKeyMetadata } from "@/lib/localDb";
import { createEmbeddingResponse, type EmbeddingHandlerOptions } from "@/lib/embeddings/service";
import { extractApiKey, isValidApiKey } from "@/sse/services/auth";
import { withInjectionGuard } from "@/middleware/promptInjectionGuard";
function toProviderScopedModelId(providerId: string, modelId: string): string {
return modelId.startsWith(`${providerId}/`) ? modelId : `${providerId}/${modelId}`;
@@ -75,7 +76,7 @@ export async function handleValidatedEmbeddingRequestBody(
return createEmbeddingResponse(body, options);
}
export async function POST(request) {
async function postHandler(request, context) {
let rawBody;
try {
rawBody = await request.json();
@@ -120,3 +121,5 @@ export async function POST(request) {
connectionId: null,
});
}
export const POST = withInjectionGuard(postHandler);

View File

@@ -2,6 +2,7 @@ import {
handleImageEdit,
handleOpenAIImageEdit,
} from "@omniroute/open-sse/handlers/imageGeneration.ts";
import { withInjectionGuard } from "@/middleware/promptInjectionGuard";
import { getProviderCredentials, clearRecoveredProviderState } from "@/sse/services/auth";
import { parseImageModel, getImageProvider } from "@omniroute/open-sse/config/imageRegistry.ts";
import { errorResponse, unavailableResponse } from "@omniroute/open-sse/utils/error.ts";
@@ -136,7 +137,7 @@ function jsonResponse(data: unknown, status = 200): Response {
});
}
export async function POST(request: Request) {
async function postHandler(request: Request, context) {
const input = await readEditInput(request);
if (!input) {
return errorResponse(
@@ -283,3 +284,5 @@ export async function POST(request: Request) {
(result as any).status
);
}
export const POST = withInjectionGuard(postHandler);

View File

@@ -1,4 +1,5 @@
import { handleImageGeneration } from "@omniroute/open-sse/handlers/imageGeneration.ts";
import { withInjectionGuard } from "@/middleware/promptInjectionGuard";
import {
getProviderCredentials,
clearRecoveredProviderState,
@@ -118,7 +119,7 @@ function publicBaseUrlHeaders(headers: Headers): Record<string, string> {
return out;
}
export async function POST(request) {
async function postHandler(request, context) {
let rawBody;
try {
rawBody = await request.json();
@@ -277,3 +278,5 @@ export async function POST(request) {
headers: { "Content-Type": "application/json" },
});
}
export const POST = withInjectionGuard(postHandler);

View File

@@ -1,5 +1,6 @@
import { handleChat } from "@/sse/handlers/chat";
import { initTranslators } from "@omniroute/open-sse/translator/index.ts";
import { withInjectionGuard } from "@/middleware/promptInjectionGuard";
let initialized = false;
@@ -29,7 +30,9 @@ export async function OPTIONS() {
/**
* POST /v1/messages - Claude format (auto convert via handleChat)
*/
export async function POST(request) {
async function postHandler(request, context) {
await ensureInitialized();
return await handleChat(request);
}
export const POST = withInjectionGuard(postHandler);

View File

@@ -1,5 +1,6 @@
import { handleModeration } from "@omniroute/open-sse/handlers/moderations.ts";
import { getProviderCredentials, clearRecoveredProviderState } from "@/sse/services/auth";
import { withInjectionGuard } from "@/middleware/promptInjectionGuard";
import { parseModerationModel } from "@omniroute/open-sse/config/moderationRegistry.ts";
import { errorResponse } from "@omniroute/open-sse/utils/error.ts";
import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts";
@@ -27,7 +28,7 @@ export async function OPTIONS() {
* POST /v1/moderations — content moderation
* OpenAI Moderations API compatible.
*/
export async function POST(request) {
async function postHandler(request, context) {
let rawBody;
try {
rawBody = await request.json();
@@ -68,3 +69,5 @@ export async function POST(request) {
}
return response;
}
export const POST = withInjectionGuard(postHandler);

View File

@@ -1,4 +1,5 @@
import { handleMusicGeneration } from "@omniroute/open-sse/handlers/musicGeneration.ts";
import { withInjectionGuard } from "@/middleware/promptInjectionGuard";
import {
getProviderCredentials,
clearRecoveredProviderState,
@@ -59,7 +60,7 @@ export async function GET() {
/**
* POST /v1/music/generations — generate music
*/
export async function POST(request) {
async function postHandler(request, context) {
let rawBody;
try {
rawBody = await request.json();
@@ -125,3 +126,5 @@ export async function POST(request) {
headers: { "Content-Type": "application/json" },
});
}
export const POST = withInjectionGuard(postHandler);

View File

@@ -1,5 +1,6 @@
import { handleRerank } from "@omniroute/open-sse/handlers/rerank.ts";
import { getProviderCredentials, clearRecoveredProviderState } from "@/sse/services/auth";
import { withInjectionGuard } from "@/middleware/promptInjectionGuard";
import { parseRerankModel, getRerankProvider } from "@omniroute/open-sse/config/rerankRegistry.ts";
import { errorResponse } from "@omniroute/open-sse/utils/error.ts";
import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts";
@@ -48,7 +49,7 @@ function buildDynamicRerankProvider(node: any) {
* Supports cloud providers (Cohere, Together, NVIDIA, Fireworks)
* and local provider_nodes (oMLX, vLLM, etc.) via dynamic routing.
*/
export async function POST(request) {
async function postHandler(request, context) {
let rawBody;
try {
rawBody = await request.json();
@@ -182,3 +183,5 @@ export async function POST(request) {
`Invalid rerank model: ${body.model}. Use format: provider/model`
);
}
export const POST = withInjectionGuard(postHandler);

View File

@@ -1,5 +1,6 @@
import { handleChat } from "@/sse/handlers/chat";
import { withEarlyStreamKeepalive } from "@omniroute/open-sse/utils/earlyStreamKeepalive";
import { withInjectionGuard } from "@/middleware/promptInjectionGuard";
import { resolveResponsesApiModel } from "@/app/api/internal/codex-responses-ws/modelResolution";
import { getModelInfo } from "@/sse/services/model";
import { getComboByName } from "@/lib/db/combos";
@@ -61,7 +62,7 @@ export async function withCodexPreferredModel(request: Request): Promise<Request
* POST /v1/responses - OpenAI Responses API format
* Handled by the unified chat handler (openai-responses format auto-detected).
*/
export async function POST(request) {
async function postHandler(request, context) {
// Codex CLI (wire_api="responses") consumes this endpoint over SSE and its reqwest
// client drops the connection if no bytes arrive within ~5s. Keep the connection
// warm with early keepalives while the upstream produces its first token (#2544).
@@ -85,3 +86,5 @@ export async function POST(request) {
}
return await handleChat(resolved);
}
export const POST = withInjectionGuard(postHandler);

View File

@@ -26,6 +26,7 @@ import {
rateLimitedProviderResponse,
type RateLimitedCredentials,
} from "@/app/api/v1/_shared/rateLimit";
import { withInjectionGuard } from "@/middleware/promptInjectionGuard";
const CORS_HEADERS = {
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
@@ -101,7 +102,7 @@ function buildDomainFilter(filters?: {
/**
* POST /v1/search — execute a web search
*/
export async function POST(request: Request) {
async function postHandler(request: Request, context: unknown) {
let rawBody: unknown;
try {
rawBody = await request.json();
@@ -319,3 +320,5 @@ class SearchError extends Error {
this.statusCode = statusCode;
}
}
export const POST = withInjectionGuard(postHandler);

View File

@@ -1,4 +1,5 @@
import { handleVideoGeneration } from "@omniroute/open-sse/handlers/videoGeneration.ts";
import { withInjectionGuard } from "@/middleware/promptInjectionGuard";
import {
getProviderCredentials,
clearRecoveredProviderState,
@@ -59,7 +60,7 @@ export async function GET() {
/**
* POST /v1/videos/generations — generate videos
*/
export async function POST(request) {
async function postHandler(request, context) {
let rawBody;
try {
rawBody = await request.json();
@@ -125,3 +126,5 @@ export async function POST(request) {
headers: { "Content-Type": "application/json" },
});
}
export const POST = withInjectionGuard(postHandler);

View File

@@ -11,6 +11,7 @@ import {
type PromptInjectionGuardrailOptions,
} from "@/lib/guardrails/promptInjection";
import { resolveDisabledGuardrails } from "@/lib/guardrails/registry";
import { CORS_HEADERS } from "@/shared/utils/cors";
/**
* Create a prompt injection guard middleware.
@@ -71,10 +72,11 @@ export function withInjectionGuard(handler: any, options: any = {}) {
error: {
message: "Request blocked: potential prompt injection detected",
type: "injection_detected",
code: "SECURITY_001",
detections: result.detections.length,
},
}),
{ status: 400, headers: { "Content-Type": "application/json" } }
{ status: 400, headers: { ...CORS_HEADERS, "Content-Type": "application/json" } }
);
}

View File

@@ -144,6 +144,20 @@ function extractMessageContents(body) {
}
}
if (typeof body.input === "string") contents.push(body.input);
if (typeof body.prompt === "string") contents.push(body.prompt);
else if (Array.isArray(body.prompt))
for (const p of body.prompt) {
if (typeof p === "string") contents.push(p);
}
if (typeof body.instructions === "string") contents.push(body.instructions);
if (typeof body.query === "string") contents.push(body.query);
if (Array.isArray(body.documents))
for (const d of body.documents) {
if (typeof d === "string") contents.push(d);
else if (d && typeof d.text === "string") contents.push(d.text);
}
return contents;
}