From 2770745618762a48d8b8425040dfdcfda7e0850b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Nov 2025 04:24:46 +0000 Subject: [PATCH] Fix multiple small bugs for security and robustness - Add input validation for hours and limit query parameters to prevent NaN and DoS attacks - Replace || with ?? for proper null coalescing in metrics summary - Fix IPv6 normalization to prevent empty string when IP is malformed - Fix stream parsing to skip empty JSON strings and avoid parse errors - Remove redundant .toString() calls on authorization header --- dashboard/app/api/metrics/route.ts | 22 ++++++++++++++-------- proxy.ts | 8 ++++---- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/dashboard/app/api/metrics/route.ts b/dashboard/app/api/metrics/route.ts index b8d72be..e65a765 100644 --- a/dashboard/app/api/metrics/route.ts +++ b/dashboard/app/api/metrics/route.ts @@ -13,8 +13,14 @@ const validatedTableName = ALLOWED_TABLES.includes(TABLE_NAME) ? TABLE_NAME : 'l export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url); - const hours = parseInt(searchParams.get('hours') || '24', 10); - const limit = parseInt(searchParams.get('limit') || '100', 10); + + // Validate and sanitize hours parameter + const hoursParam = parseInt(searchParams.get('hours') || '24', 10); + const hours = !isNaN(hoursParam) && hoursParam > 0 && hoursParam <= 720 ? hoursParam : 24; + + // Validate and sanitize limit parameter + const limitParam = parseInt(searchParams.get('limit') || '100', 10); + const limit = !isNaN(limitParam) && limitParam > 0 && limitParam <= 1000 ? limitParam : 100; try { const client = await pool.connect(); @@ -93,12 +99,12 @@ export async function GET(request: NextRequest) { success: true, data: { summary: { - totalRequests: parseInt(summary.total_requests || '0'), - totalTokens: parseInt(summary.total_tokens_used || '0'), - totalCost: parseFloat(summary.total_cost || '0'), - avgResponseTime: parseFloat(summary.avg_response_time || '0'), - uniqueModels: parseInt(summary.unique_models || '0'), - uniqueClients: parseInt(summary.unique_clients || '0'), + totalRequests: parseInt(summary.total_requests ?? '0'), + totalTokens: parseInt(summary.total_tokens_used ?? '0'), + totalCost: parseFloat(summary.total_cost ?? '0'), + avgResponseTime: parseFloat(summary.avg_response_time ?? '0'), + uniqueModels: parseInt(summary.unique_models ?? '0'), + uniqueClients: parseInt(summary.unique_clients ?? '0'), }, recentRequests, modelBreakdown, diff --git a/proxy.ts b/proxy.ts index ecd6acf..f3d0bfb 100644 --- a/proxy.ts +++ b/proxy.ts @@ -33,7 +33,7 @@ function generateRequestId(): string { function normalizeIp(ip: string | null | undefined): string | null { if (!ip) return null; // Handle IPv6-mapped IPv4 addresses (::ffff:x.x.x.x) - if (ip.startsWith('::ffff:')) { + if (ip.startsWith('::ffff:') && ip.length > 7) { return ip.substring(7); } return ip; @@ -92,7 +92,7 @@ const server = http.createServer(async (req, res) => { } const auth = req.headers["authorization"]; - if (!auth?.toString().startsWith("Bearer ")) { + if (!auth?.startsWith("Bearer ")) { res.statusCode = 401; res.end(JSON.stringify({ error: "Missing or invalid Authorization header" })); return; @@ -109,7 +109,7 @@ const server = http.createServer(async (req, res) => { method, headers: { "Content-Type": (req.headers["content-type"] as string) || "application/json", - Authorization: auth.toString(), + Authorization: auth, }, // @ts-ignore duplex: "half", @@ -156,7 +156,7 @@ const server = http.createServer(async (req, res) => { for (const line of lines) { if (!line.startsWith("data:")) continue; const jsonStr = line.slice(5).trim(); - if (jsonStr === "[DONE]") continue; + if (jsonStr === "[DONE]" || jsonStr === "") continue; try { const obj = JSON.parse(jsonStr); if (obj.usage) usageFromStream = obj.usage;