diff --git a/src/app/api/provider-nodes/validate/route.js b/src/app/api/provider-nodes/validate/route.js index 0d7882ae..4148ab41 100644 --- a/src/app/api/provider-nodes/validate/route.js +++ b/src/app/api/provider-nodes/validate/route.js @@ -1,4 +1,6 @@ import { NextResponse } from "next/server"; +import { assertPublicUrl } from "@/shared/utils/ssrfGuard.js"; +import { isLocalRequest } from "@/dashboardGuard"; // Fetch with timeout wrapper const fetchWithTimeout = (url, options, timeout = 10000) => { @@ -64,6 +66,15 @@ export async function POST(request) { return NextResponse.json({ error: "Invalid URL format" }, { status: 400 }); } + // SSRF guard for remote callers; local host keeps self-hosted nodes (e.g. ollama-local) + if (!isLocalRequest(request)) { + try { + assertPublicUrl(baseUrl); + } catch { + return NextResponse.json({ error: "URL not allowed" }, { status: 400 }); + } + } + // Custom Embedding Validation - test POST /embeddings directly if (type === "custom-embedding") { const normalizedBase = baseUrl.trim().replace(/\/$/, ""); diff --git a/src/app/api/settings/route.js b/src/app/api/settings/route.js index bc69e0d8..ee2682ca 100644 --- a/src/app/api/settings/route.js +++ b/src/app/api/settings/route.js @@ -11,6 +11,9 @@ const SETTINGS_RESPONSE_HEADERS = { "Cache-Control": "no-store" }; +// Secrets must never be mass-assigned from request body (CWE-915) +const PROTECTED_SETTING_KEYS = ["password", "mitmSudoEncrypted"]; + export async function GET() { try { const settings = await getSettings(); @@ -36,6 +39,9 @@ export async function PATCH(request) { try { const body = await request.json(); + // Strip protected secrets before any internal handling sets them + for (const key of PROTECTED_SETTING_KEYS) delete body[key]; + // If updating password, hash it if (body.newPassword) { const settings = await getSettings();