diff --git a/package-lock.json b/package-lock.json index 3d581597d5..cf07955fb3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1883,9 +1883,9 @@ } }, "node_modules/@hono/node-server": { - "version": "1.19.12", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.12.tgz", - "integrity": "sha512-txsUW4SQ1iilgE0l9/e9VQWmELXifEFvmdA1j6WFh/aFPj99hIntrSsq/if0UWyGVkmrRPKA1wCeP+UCr1B9Uw==", + "version": "1.19.13", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.13.tgz", + "integrity": "sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==", "license": "MIT", "engines": { "node": ">=18.14.1" @@ -11959,9 +11959,9 @@ "license": "MIT" }, "node_modules/hono": { - "version": "4.12.9", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.9.tgz", - "integrity": "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==", + "version": "4.12.12", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.12.tgz", + "integrity": "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==", "license": "MIT", "engines": { "node": ">=16.9.0" diff --git a/package.json b/package.json index ad63f47811..e454e205a4 100644 --- a/package.json +++ b/package.json @@ -176,6 +176,8 @@ "lodash-es": "^4.18.1", "dompurify": "^3.3.2", "path-to-regexp": "^8.4.0", + "hono": "^4.12.12", + "@hono/node-server": "^1.19.13", "react": "$react", "react-dom": "$react-dom" } diff --git a/src/app/api/providers/[id]/sync-models/route.ts b/src/app/api/providers/[id]/sync-models/route.ts index 95550c1d04..8dd1c46e35 100644 --- a/src/app/api/providers/[id]/sync-models/route.ts +++ b/src/app/api/providers/[id]/sync-models/route.ts @@ -146,10 +146,17 @@ export async function POST(request: Request, { params }: { params: Promise<{ id: logProvider = toNonEmptyString(connection.provider) || "unknown"; channelLabel = getModelSyncChannelLabel(connection); - // Fetch models from the existing /api/providers/[id]/models endpoint via localhost to prevent SSRF - const safeOrigin = `http://127.0.0.1:${process.env.PORT || 20128}`; - const modelsUrl = `${safeOrigin}/api/providers/${encodeURIComponent(id)}/models`; - const modelsRes = await fetch(modelsUrl, { + // Fetch models from the existing /api/providers/[id]/models endpoint. + // Construct a safe localhost URL from the incoming request's origin. + // The route only accepts authenticated or internal-scheduler requests, + // and the path is hardcoded — no user-controlled URL components reach fetch. + const SAFE_HOSTS = new Set(["localhost", "127.0.0.1", "0.0.0.0", "::1"]); + const incomingUrl = new URL(request.url); + const safeOrigin = SAFE_HOSTS.has(incomingUrl.hostname) + ? incomingUrl.origin + : `http://127.0.0.1:${process.env.PORT || "20128"}`; + const modelsPath = `/api/providers/${encodeURIComponent(id)}/models`; + const modelsRes = await fetch(new URL(modelsPath, safeOrigin).href, { method: "GET", headers: { cookie: request.headers.get("cookie") || "", diff --git a/tests/unit/context-manager.test.mjs b/tests/unit/context-manager.test.mjs index caa6611626..1ce7125681 100644 --- a/tests/unit/context-manager.test.mjs +++ b/tests/unit/context-manager.test.mjs @@ -23,7 +23,7 @@ test("getTokenLimit: detects claude", () => { }); test("getTokenLimit: detects gemini", () => { - assert.equal(getTokenLimit("gemini", "gemini-2.5-pro"), 1000000); + assert.equal(getTokenLimit("gemini", "gemini-2.5-pro"), 1048576); }); test("getTokenLimit: default fallback", () => {