fix(security): restrict local connector endpoints
This commit is contained in:
1 parent
fa19e95f19
commit
84b92c859e
12 files changed
+158
-18
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { getProviders } from "@mariozechner/pi-ai";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
CONNECTORS,
|
||||
getConnectorKnownProvider,
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
isLocalModelResolverProvider,
|
||||
isPiKnownProvider,
|
||||
} from "../../src/main/connectors";
|
||||
import { FIREWORKS_FIREPASS_MODEL_ID, resolveModel, resolveModels } from "../../src/main/model-resolver";
|
||||
import { FIREWORKS_FIREPASS_MODEL_ID, fetchLocalConnectorModels, resolveModel, resolveModels } from "../../src/main/model-resolver";
|
||||
|
||||
describe("connector provider guard", () => {
|
||||
it("classifies catalog providers before they enter pi-ai APIs", () => {
|
||||
@@ -38,6 +38,18 @@ describe("connector provider guard", () => {
|
||||
expect(resolveModels("not-a-pi-provider")).toEqual([]);
|
||||
expect(resolveModel("not-a-pi-provider", "model-id")).toBeNull();
|
||||
expect(resolveModel("fireworks-firepass", "ignored")?.id).toBe(FIREWORKS_FIREPASS_MODEL_ID);
|
||||
expect(resolveModel("ollama", "llama3.2", { baseUrl: "http://127.0.0.1:11434/v1" })?.baseUrl).toBe("http://127.0.0.1:11434/v1");
|
||||
expect(resolveModel("ollama", "llama3.2", { baseUrl: "https://example.com/v1" })).toBeNull();
|
||||
});
|
||||
|
||||
it("rejects remote URLs before local model catalog fetch", async () => {
|
||||
const fetchSpy = vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("fetch should stay unused"));
|
||||
try {
|
||||
await expect(fetchLocalConnectorModels("https://example.com/v1")).resolves.toEqual([]);
|
||||
expect(fetchSpy).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
fetchSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps provider calls free of erased never casts", () => {
|
||||
|
||||
@@ -157,4 +157,26 @@ describe("CredentialResolver", () => {
|
||||
GOOGLE_APPLICATION_CREDENTIALS: "/etc/gcp/service-account.json",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects remote baseUrl values for local connectors", async () => {
|
||||
store.save("ollama", {
|
||||
kind: "local",
|
||||
value: { baseUrl: "https://example.com/v1" },
|
||||
});
|
||||
|
||||
let thrown: unknown;
|
||||
try {
|
||||
await resolver.resolve("ollama");
|
||||
} catch (error) {
|
||||
thrown = error;
|
||||
}
|
||||
|
||||
expect(thrown).toBeInstanceOf(CredentialError);
|
||||
expect(thrown).toMatchObject({
|
||||
kind: "upstream",
|
||||
code: "CREDENTIAL_INVALID",
|
||||
connectorId: "ollama",
|
||||
context: { connectorId: "ollama" },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
azureCredentialSchema,
|
||||
credentialPayloadSchema,
|
||||
gcpAdcCredentialSchema,
|
||||
isLocalConnectorBaseUrl,
|
||||
localCredentialSchema,
|
||||
removeCredentialInputSchema,
|
||||
saveCredentialInputSchema,
|
||||
testCredentialInputSchema,
|
||||
@@ -59,6 +61,39 @@ describe("credentials Zod schemas", () => {
|
||||
expect(gcpAdcCredentialSchema.safeParse({ project: "p", location: "us-central1" }).success).toBe(true);
|
||||
});
|
||||
|
||||
it("localCredentialSchema accepts loopback and private-network baseUrl values", () => {
|
||||
for (const baseUrl of [
|
||||
"http://localhost:11434/v1",
|
||||
"http://127.0.0.1:11434/v1",
|
||||
"http://10.0.0.20:11434/v1",
|
||||
"https://172.16.0.10:11434/v1",
|
||||
"http://172.31.255.255:11434/v1",
|
||||
"http://192.168.1.20:11434/v1",
|
||||
"http://169.254.1.20:11434/v1",
|
||||
"http://[::1]:11434/v1",
|
||||
"http://[fc00::1]:11434/v1",
|
||||
"http://[fd12::1]:11434/v1",
|
||||
"http://[fe80::1]:11434/v1",
|
||||
]) {
|
||||
expect(localCredentialSchema.safeParse({ baseUrl }).success).toBe(true);
|
||||
expect(isLocalConnectorBaseUrl(baseUrl)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("localCredentialSchema rejects remote OpenAI-compatible endpoints", () => {
|
||||
for (const baseUrl of [
|
||||
"https://api.openai.com/v1",
|
||||
"https://example.com/v1",
|
||||
"http://172.32.0.1:11434/v1",
|
||||
"http://100.64.0.1:11434/v1",
|
||||
"ftp://localhost:11434/v1",
|
||||
"http://user:pass@127.0.0.1:11434/v1",
|
||||
]) {
|
||||
expect(localCredentialSchema.safeParse({ baseUrl }).success).toBe(false);
|
||||
expect(isLocalConnectorBaseUrl(baseUrl)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("credentialPayloadSchema discriminates on kind", () => {
|
||||
expect(
|
||||
credentialPayloadSchema.safeParse({
|
||||
|
||||
Reference in new issue
Block a user