fix(backend): harden provider credential isolation
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { test, expect } from "vitest";
|
||||
import { spawn } from "node:child_process";
|
||||
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
|
||||
import { chmodSync, mkdtempSync, writeFileSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import path from "node:path";
|
||||
@@ -82,13 +82,16 @@ test("production model-list spawn preserves PATH and passes the portable data ro
|
||||
}
|
||||
});
|
||||
|
||||
test("model-list spawn omits ambient THT_DATA_ROOT when config does not provide one", async () => {
|
||||
test("model-list spawn scrubs ambient provider credentials and generic secret metadata", async () => {
|
||||
const script = scriptWith([]);
|
||||
const calls: any[][] = [];
|
||||
const previousDataRoot = process.env.THT_DATA_ROOT;
|
||||
const previousCredential = process.env.PI_PROVIDER_API_KEY;
|
||||
process.env.THT_DATA_ROOT = "/ambient-must-not-leak";
|
||||
process.env.PI_PROVIDER_API_KEY = "still-inherited";
|
||||
process.env.PI_PROVIDER_API_KEY = "must-not-leak";
|
||||
process.env.OPENAI_API_KEY = "must-not-leak";
|
||||
process.env.AWS_SECRET_ACCESS_KEY = "must-not-leak";
|
||||
process.env.CLOUDFLARE_ACCOUNT_ID = "must-not-leak";
|
||||
try {
|
||||
const lister = createPiModelLister(loadConfig({ PI_BIN: "/usr/local/bin/pi" }), {
|
||||
spawnFn: (...args: any[]) => {
|
||||
@@ -98,12 +101,43 @@ test("model-list spawn omits ambient THT_DATA_ROOT when config does not provide
|
||||
});
|
||||
await lister();
|
||||
expect(calls[0][2].env).not.toHaveProperty("THT_DATA_ROOT");
|
||||
expect(calls[0][2].env.PI_PROVIDER_API_KEY).toBe("still-inherited");
|
||||
expect(calls[0][2].env).not.toHaveProperty("PI_PROVIDER_API_KEY");
|
||||
expect(calls[0][2].env).not.toHaveProperty("OPENAI_API_KEY");
|
||||
expect(calls[0][2].env).not.toHaveProperty("AWS_SECRET_ACCESS_KEY");
|
||||
expect(calls[0][2].env).not.toHaveProperty("CLOUDFLARE_ACCOUNT_ID");
|
||||
} finally {
|
||||
if (previousDataRoot === undefined) delete process.env.THT_DATA_ROOT;
|
||||
else process.env.THT_DATA_ROOT = previousDataRoot;
|
||||
if (previousCredential === undefined) delete process.env.PI_PROVIDER_API_KEY;
|
||||
else process.env.PI_PROVIDER_API_KEY = previousCredential;
|
||||
delete process.env.OPENAI_API_KEY;
|
||||
delete process.env.AWS_SECRET_ACCESS_KEY;
|
||||
delete process.env.CLOUDFLARE_ACCOUNT_ID;
|
||||
rmSync(path.dirname(script), { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("model-list spawn loads only the selected canonical provider credential", async () => {
|
||||
const script = scriptWith([]);
|
||||
const secret = join(path.dirname(script), "model-key");
|
||||
writeFileSync(secret, "selected-secret", { mode: 0o600 });
|
||||
chmodSync(secret, 0o600);
|
||||
const calls: any[][] = [];
|
||||
const lister = createPiModelLister(loadConfig({
|
||||
PI_PROVIDER: "Gemini",
|
||||
THT_MODEL_API_KEY_FILE: secret,
|
||||
}), {
|
||||
spawnFn: (...args: any[]) => {
|
||||
calls.push(args);
|
||||
return spawn("node", [FAKE, script]) as any;
|
||||
},
|
||||
});
|
||||
try {
|
||||
await lister();
|
||||
expect(calls[0][2].env.GEMINI_API_KEY).toBe("selected-secret");
|
||||
expect(calls[0][2].env).not.toHaveProperty("THT_MODEL_API_KEY_FILE");
|
||||
expect(JSON.stringify(calls[0].slice(0, 2))).not.toContain("selected-secret");
|
||||
} finally {
|
||||
rmSync(path.dirname(script), { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user