fix(security): restrict local connector endpoints

This commit is contained in:
ZacharyZhang-NY committed 2026-05-15 00:43:45 -04:00
1 parent fa19e95f19
commit 84b92c859e
12 files changed
+158 -18

No files matched your search

+2 -2
View File
@@ -25,7 +25,7 @@ export const LOCAL_CONNECTORS: ConnectorDefinition[] = [
defaultModel: "llama3.2",
auth: { type: "local", defaultBaseUrl: "http://localhost:11434/v1" },
notes:
"Ollama exposes /v1/chat/completions and /v1/models. The default port is 11434; override if you launched `ollama serve` on a different address.",
"Ollama exposes /v1/chat/completions and /v1/models. The default port is 11434; override with a loopback or private-network address.",
},
{
id: "lm-studio",
@@ -36,6 +36,6 @@ export const LOCAL_CONNECTORS: ConnectorDefinition[] = [
defaultModel: "local-model",
auth: { type: "local", defaultBaseUrl: "http://localhost:1234/v1" },
notes:
"Enable LM Studio's local server from the Developer tab; the default port is 1234. Model ids match whatever you have loaded.",
"Enable LM Studio's local server from the Developer tab; the default port is 1234. Use a loopback or private-network address.",
},
];
@@ -1,5 +1,6 @@
import { getOAuthProvider } from "@mariozechner/pi-ai/oauth";
import { getEnvApiKey } from "@mariozechner/pi-ai";
import { isLocalConnectorBaseUrl } from "@shared/contracts/schemas-credentials";
import { getConnectorDefinition, isPiKnownProvider } from "@main/connectors";
import { UpstreamError } from "../errors/index.js";
import { CredentialStore } from "./credential-store";
@@ -150,6 +151,13 @@ export class CredentialResolver {
if (stored?.kind !== "local") {
throw new CredentialError("CREDENTIAL_MISSING", connectorId);
}
if (!isLocalConnectorBaseUrl(stored.value.baseUrl)) {
throw new CredentialError(
"CREDENTIAL_INVALID",
connectorId,
"Local connector baseUrl must use localhost, loopback, or a private IP host.",
);
}
// Ollama and LM Studio ignore the bearer value but the OpenAI SDK
// refuses an empty string, so we hand it the connector id as a
// stable non-empty placeholder. The actual transport is keyed off
+9
View File
@@ -18,6 +18,7 @@
import log from "electron-log/main.js";
import { getModels, type Api, type Model } from "@mariozechner/pi-ai";
import { isLocalConnectorBaseUrl } from "@shared/contracts/schemas-credentials";
import { isPiKnownProvider } from "./connectors";
// Fireworks' OpenAI-compatible root. The OpenAI SDK appends
@@ -125,6 +126,10 @@ export function resolveModel(
log.warn(`[model-resolver] ${provider} requires a baseUrl from credentials; none provided.`);
return null;
}
if (!isLocalConnectorBaseUrl(options.baseUrl)) {
log.warn(`[model-resolver] ${provider} rejected non-local baseUrl.`);
return null;
}
return buildLocalModel(provider, modelId, options.baseUrl);
}
if (!isPiKnownProvider(provider)) {
@@ -221,6 +226,10 @@ export function resolveModels(provider: string): Array<{ id: string; name: strin
export async function fetchLocalConnectorModels(
baseUrl: string,
): Promise<Array<{ id: string; name: string }>> {
if (!isLocalConnectorBaseUrl(baseUrl)) {
log.warn("[model-resolver] rejected non-local model catalog baseUrl.");
return [];
}
const url = `${baseUrl.replace(/\/$/, "")}/models`;
try {
const response = await fetch(url, { method: "GET" });