* fix(security): prevent String(undefined) coercion in credential inputs When a prompter returns undefined (due to cancel, timeout, or bug), String(undefined).trim() produces the literal string "undefined" instead of "". This truthy string prevents secure fallbacks from triggering, allowing predictable credential values (e.g., gateway password = "undefined"). Fix all 8 occurrences by using String(value ?? "").trim(), which correctly yields "" for null/undefined inputs and triggers downstream validation or fallback logic. Fixes #8054 * fix(security): also fix String(undefined) in api-provider credential inputs Address codex review feedback: 4 additional occurrences of the unsafe String(variable).trim() pattern in auth-choice.apply.api-providers.ts (Cloudflare Account ID, Gateway ID, synthetic API key inputs + validators). * fix(test): strengthen password coercion test per review feedback * fix(security): harden credential prompt coercion --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
126 lines
3.8 KiB
TypeScript
126 lines
3.8 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import type { RuntimeEnv } from "../runtime.js";
|
|
import type { WizardPrompter } from "./prompts.js";
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
randomToken: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("../commands/onboard-helpers.js", async (importActual) => {
|
|
const actual = await importActual<typeof import("../commands/onboard-helpers.js")>();
|
|
return {
|
|
...actual,
|
|
randomToken: mocks.randomToken,
|
|
};
|
|
});
|
|
|
|
vi.mock("../infra/tailscale.js", () => ({
|
|
findTailscaleBinary: vi.fn(async () => undefined),
|
|
}));
|
|
|
|
import { configureGatewayForOnboarding } from "./onboarding.gateway-config.js";
|
|
|
|
describe("configureGatewayForOnboarding", () => {
|
|
it("generates a token when the prompt returns undefined", async () => {
|
|
mocks.randomToken.mockReturnValue("generated-token");
|
|
|
|
const selectQueue = ["loopback", "token", "off"];
|
|
const textQueue = ["18789", undefined];
|
|
const prompter: WizardPrompter = {
|
|
intro: vi.fn(async () => {}),
|
|
outro: vi.fn(async () => {}),
|
|
note: vi.fn(async () => {}),
|
|
select: vi.fn(async () => selectQueue.shift() as string),
|
|
multiselect: vi.fn(async () => []),
|
|
text: vi.fn(async () => textQueue.shift() as string),
|
|
confirm: vi.fn(async () => false),
|
|
progress: vi.fn(() => ({ update: vi.fn(), stop: vi.fn() })),
|
|
};
|
|
|
|
const runtime: RuntimeEnv = {
|
|
log: vi.fn(),
|
|
error: vi.fn(),
|
|
exit: vi.fn(),
|
|
};
|
|
|
|
const result = await configureGatewayForOnboarding({
|
|
flow: "advanced",
|
|
baseConfig: {},
|
|
nextConfig: {},
|
|
localPort: 18789,
|
|
quickstartGateway: {
|
|
hasExisting: false,
|
|
port: 18789,
|
|
bind: "loopback",
|
|
authMode: "token",
|
|
tailscaleMode: "off",
|
|
token: undefined,
|
|
password: undefined,
|
|
customBindHost: undefined,
|
|
tailscaleResetOnExit: false,
|
|
},
|
|
prompter,
|
|
runtime,
|
|
});
|
|
|
|
expect(result.settings.gatewayToken).toBe("generated-token");
|
|
expect(result.nextConfig.gateway?.nodes?.denyCommands).toEqual([
|
|
"camera.snap",
|
|
"camera.clip",
|
|
"screen.record",
|
|
"calendar.add",
|
|
"contacts.add",
|
|
"reminders.add",
|
|
]);
|
|
});
|
|
it("does not set password to literal 'undefined' when prompt returns undefined", async () => {
|
|
mocks.randomToken.mockReturnValue("unused");
|
|
|
|
// Flow: loopback bind → password auth → tailscale off
|
|
const selectQueue = ["loopback", "password", "off"];
|
|
// Port prompt → OK, then password prompt → returns undefined
|
|
const textQueue = ["18789", undefined];
|
|
const prompter: WizardPrompter = {
|
|
intro: vi.fn(async () => {}),
|
|
outro: vi.fn(async () => {}),
|
|
note: vi.fn(async () => {}),
|
|
select: vi.fn(async () => selectQueue.shift() as string),
|
|
multiselect: vi.fn(async () => []),
|
|
text: vi.fn(async () => textQueue.shift() as string),
|
|
confirm: vi.fn(async () => false),
|
|
progress: vi.fn(() => ({ update: vi.fn(), stop: vi.fn() })),
|
|
};
|
|
|
|
const runtime: RuntimeEnv = {
|
|
log: vi.fn(),
|
|
error: vi.fn(),
|
|
exit: vi.fn(),
|
|
};
|
|
|
|
const result = await configureGatewayForOnboarding({
|
|
flow: "advanced",
|
|
baseConfig: {},
|
|
nextConfig: {},
|
|
localPort: 18789,
|
|
quickstartGateway: {
|
|
hasExisting: false,
|
|
port: 18789,
|
|
bind: "loopback",
|
|
authMode: "password",
|
|
tailscaleMode: "off",
|
|
token: undefined,
|
|
password: undefined,
|
|
customBindHost: undefined,
|
|
tailscaleResetOnExit: false,
|
|
},
|
|
prompter,
|
|
runtime,
|
|
});
|
|
|
|
const authConfig = result.nextConfig.gateway?.auth as { mode?: string; password?: string };
|
|
expect(authConfig?.mode).toBe("password");
|
|
expect(authConfig?.password).toBe("");
|
|
expect(authConfig?.password).not.toBe("undefined");
|
|
});
|
|
});
|