From 6446454cd7056b79aafb533b9bbdeba14a9d976e Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Tue, 23 Dec 2025 00:26:01 +0900 Subject: [PATCH] fix: add SSRF protection to validate-model endpoint (#357) Block private IPs, localhost, cloud metadata endpoints (169.254.169.254), and internal hostnames in custom baseUrl parameter to prevent server-side request forgery attacks. --- app/api/validate-model/route.ts | 68 +++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/app/api/validate-model/route.ts b/app/api/validate-model/route.ts index 7beeaa4..1328035 100644 --- a/app/api/validate-model/route.ts +++ b/app/api/validate-model/route.ts @@ -11,6 +11,66 @@ import { createOllama } from "ollama-ai-provider-v2" export const runtime = "nodejs" +/** + * SECURITY: Check if URL points to private/internal network (SSRF protection) + * Blocks: localhost, private IPs, link-local, AWS metadata service + */ +function isPrivateUrl(urlString: string): boolean { + try { + const url = new URL(urlString) + const hostname = url.hostname.toLowerCase() + + // Block localhost + if ( + hostname === "localhost" || + hostname === "127.0.0.1" || + hostname === "::1" + ) { + return true + } + + // Block AWS/cloud metadata endpoints + if ( + hostname === "169.254.169.254" || + hostname === "metadata.google.internal" + ) { + return true + } + + // Check for private IPv4 ranges + const ipv4Match = hostname.match( + /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/, + ) + if (ipv4Match) { + const [, a, b] = ipv4Match.map(Number) + // 10.0.0.0/8 + if (a === 10) return true + // 172.16.0.0/12 + if (a === 172 && b >= 16 && b <= 31) return true + // 192.168.0.0/16 + if (a === 192 && b === 168) return true + // 169.254.0.0/16 (link-local) + if (a === 169 && b === 254) return true + // 127.0.0.0/8 (loopback) + if (a === 127) return true + } + + // Block common internal hostnames + if ( + hostname.endsWith(".local") || + hostname.endsWith(".internal") || + hostname.endsWith(".localhost") + ) { + return true + } + + return false + } catch { + // Invalid URL - block it + return true + } +} + interface ValidateRequest { provider: string apiKey: string @@ -42,6 +102,14 @@ export async function POST(req: Request) { ) } + // SECURITY: Block SSRF attacks via custom baseUrl + if (baseUrl && isPrivateUrl(baseUrl)) { + return NextResponse.json( + { valid: false, error: "Invalid base URL" }, + { status: 400 }, + ) + } + // Validate credentials based on provider if (provider === "bedrock") { if (!awsAccessKeyId || !awsSecretAccessKey || !awsRegion) {