fix(security): prevent String(undefined) coercion in credential inputs (#12287)

* 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>
This commit is contained in:
Marcus Castro
2026-02-13 00:25:05 -03:00
committed by GitHub
parent 63bb1e02b0
commit ec44e262be
9 changed files with 207 additions and 29 deletions

View File

@@ -73,4 +73,53 @@ describe("configureGatewayForOnboarding", () => {
"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");
});
});

View File

@@ -217,7 +217,7 @@ export async function configureGatewayForOnboarding(
auth: {
...nextConfig.gateway?.auth,
mode: "password",
password: String(password).trim(),
password: String(password ?? "").trim(),
},
},
};