From 02dc8ea0f3b4fef023e30e015208a6e58829963e Mon Sep 17 00:00:00 2001 From: nyatoru Date: Mon, 23 Feb 2026 04:00:22 +0700 Subject: [PATCH] fix(api): enhance authentication for /models endpoint with stricter checks --- src/app/api/v1/models/route.ts | 54 ++++++++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/src/app/api/v1/models/route.ts b/src/app/api/v1/models/route.ts index 613a0a8d68..3db7c4d0d6 100644 --- a/src/app/api/v1/models/route.ts +++ b/src/app/api/v1/models/route.ts @@ -99,8 +99,9 @@ export async function OPTIONS() { */ export async function GET(request: Request) { try { - // Issue #100: Optionally require API key for /models (security hardening) - // When enabled, unauthenticated requests get 404 to hide endpoint existence + // Issue #100: Optionally require authentication for /models (security hardening) + // When enabled, unauthenticated requests get 401 with proper error response. + // Supports API key (Bearer token) for external clients and JWT cookie for dashboard. let settings: Record = {}; try { settings = await getSettings(); @@ -121,12 +122,53 @@ export async function GET(request: Request) { const origin = request.headers.get("origin"); const referer = request.headers.get("referer"); const host = request.headers.get("host"); + const secFetchSite = request.headers.get("sec-fetch-site"); // Check if request is from dashboard (same origin) - const isDashboardRequest = - !origin || // No origin header = same-origin request (browser navigation) - (host && origin.includes(host)) || // Origin matches host - (referer && host && referer.includes(host)); // Referer matches host + // Security: Use strict matching instead of loose includes() + let isDashboardRequest = false; + + // Check sec-fetch-site header (set by browser, harder to spoof) + // "same-origin" = same origin request, "none" = same origin navigation + if (secFetchSite === "same-origin" || secFetchSite === "none") { + isDashboardRequest = true; + } + + // Fallback: Check origin/referer against host with proper URL parsing + if (!isDashboardRequest && host) { + const normalizeHost = (h: string) => { + // Remove port if present for comparison + const colonIndex = h.lastIndexOf(":"); + return colonIndex > 0 ? h.slice(0, colonIndex) : h; + }; + const normalizedHost = normalizeHost(host); + + if (origin) { + try { + const originUrl = new URL(origin); + const normalizedOrigin = normalizeHost(originUrl.host); + // Exact match only + if (normalizedOrigin === normalizedHost) { + isDashboardRequest = true; + } + } catch { + // Invalid URL, not a dashboard request + } + } + + if (!isDashboardRequest && referer) { + try { + const refererUrl = new URL(referer); + const normalizedReferer = normalizeHost(refererUrl.host); + // Exact match only + if (normalizedReferer === normalizedHost) { + isDashboardRequest = true; + } + } catch { + // Invalid URL, not a dashboard request + } + } + } if (isDashboardRequest) { try {