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

@@ -177,7 +177,7 @@ export async function applyAuthChoiceApiProviders(
message: "Enter OpenRouter API key",
validate: validateApiKeyInput,
});
await setOpenrouterApiKey(normalizeApiKeyInput(String(key)), params.agentDir);
await setOpenrouterApiKey(normalizeApiKeyInput(String(key ?? "")), params.agentDir);
hasCredential = true;
}
@@ -242,7 +242,7 @@ export async function applyAuthChoiceApiProviders(
message: "Enter LiteLLM API key",
validate: validateApiKeyInput,
});
await setLitellmApiKey(normalizeApiKeyInput(String(key)), params.agentDir);
await setLitellmApiKey(normalizeApiKeyInput(String(key ?? "")), params.agentDir);
hasCredential = true;
}
}
@@ -296,7 +296,7 @@ export async function applyAuthChoiceApiProviders(
message: "Enter Vercel AI Gateway API key",
validate: validateApiKeyInput,
});
await setVercelAiGatewayApiKey(normalizeApiKeyInput(String(key)), params.agentDir);
await setVercelAiGatewayApiKey(normalizeApiKeyInput(String(key ?? "")), params.agentDir);
}
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "vercel-ai-gateway:default",
@@ -329,16 +329,16 @@ export async function applyAuthChoiceApiProviders(
if (!accountId) {
const value = await params.prompter.text({
message: "Enter Cloudflare Account ID",
validate: (val) => (String(val).trim() ? undefined : "Account ID is required"),
validate: (val) => (String(val ?? "").trim() ? undefined : "Account ID is required"),
});
accountId = String(value).trim();
accountId = String(value ?? "").trim();
}
if (!gatewayId) {
const value = await params.prompter.text({
message: "Enter Cloudflare AI Gateway ID",
validate: (val) => (String(val).trim() ? undefined : "Gateway ID is required"),
validate: (val) => (String(val ?? "").trim() ? undefined : "Gateway ID is required"),
});
gatewayId = String(value).trim();
gatewayId = String(value ?? "").trim();
}
};
@@ -381,7 +381,7 @@ export async function applyAuthChoiceApiProviders(
await setCloudflareAiGatewayConfig(
accountId,
gatewayId,
normalizeApiKeyInput(String(key)),
normalizeApiKeyInput(String(key ?? "")),
params.agentDir,
);
hasCredential = true;
@@ -443,7 +443,7 @@ export async function applyAuthChoiceApiProviders(
message: "Enter Moonshot API key",
validate: validateApiKeyInput,
});
await setMoonshotApiKey(normalizeApiKeyInput(String(key)), params.agentDir);
await setMoonshotApiKey(normalizeApiKeyInput(String(key ?? "")), params.agentDir);
}
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "moonshot:default",
@@ -490,7 +490,7 @@ export async function applyAuthChoiceApiProviders(
message: "Enter Moonshot API key (.cn)",
validate: validateApiKeyInput,
});
await setMoonshotApiKey(normalizeApiKeyInput(String(key)), params.agentDir);
await setMoonshotApiKey(normalizeApiKeyInput(String(key ?? "")), params.agentDir);
}
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "moonshot:default",
@@ -550,7 +550,7 @@ export async function applyAuthChoiceApiProviders(
message: "Enter Kimi Coding API key",
validate: validateApiKeyInput,
});
await setKimiCodingApiKey(normalizeApiKeyInput(String(key)), params.agentDir);
await setKimiCodingApiKey(normalizeApiKeyInput(String(key ?? "")), params.agentDir);
}
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "kimi-coding:default",
@@ -598,7 +598,7 @@ export async function applyAuthChoiceApiProviders(
message: "Enter Gemini API key",
validate: validateApiKeyInput,
});
await setGeminiApiKey(normalizeApiKeyInput(String(key)), params.agentDir);
await setGeminiApiKey(normalizeApiKeyInput(String(key ?? "")), params.agentDir);
}
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "google:default",
@@ -666,7 +666,7 @@ export async function applyAuthChoiceApiProviders(
message: "Enter Z.AI API key",
validate: validateApiKeyInput,
});
apiKey = normalizeApiKeyInput(String(key));
apiKey = normalizeApiKeyInput(String(key ?? ""));
await setZaiApiKey(apiKey, params.agentDir);
}
@@ -763,7 +763,7 @@ export async function applyAuthChoiceApiProviders(
message: "Enter Xiaomi API key",
validate: validateApiKeyInput,
});
await setXiaomiApiKey(normalizeApiKeyInput(String(key)), params.agentDir);
await setXiaomiApiKey(normalizeApiKeyInput(String(key ?? "")), params.agentDir);
}
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "xiaomi:default",
@@ -789,13 +789,13 @@ export async function applyAuthChoiceApiProviders(
if (authChoice === "synthetic-api-key") {
if (params.opts?.token && params.opts?.tokenProvider === "synthetic") {
await setSyntheticApiKey(String(params.opts.token).trim(), params.agentDir);
await setSyntheticApiKey(String(params.opts.token ?? "").trim(), params.agentDir);
} else {
const key = await params.prompter.text({
message: "Enter Synthetic API key",
validate: (value) => (value?.trim() ? undefined : "Required"),
});
await setSyntheticApiKey(String(key).trim(), params.agentDir);
await setSyntheticApiKey(String(key ?? "").trim(), params.agentDir);
}
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "synthetic:default",
@@ -854,7 +854,7 @@ export async function applyAuthChoiceApiProviders(
message: "Enter Venice AI API key",
validate: validateApiKeyInput,
});
await setVeniceApiKey(normalizeApiKeyInput(String(key)), params.agentDir);
await setVeniceApiKey(normalizeApiKeyInput(String(key ?? "")), params.agentDir);
}
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "venice:default",
@@ -911,7 +911,7 @@ export async function applyAuthChoiceApiProviders(
message: "Enter OpenCode Zen API key",
validate: validateApiKeyInput,
});
await setOpencodeZenApiKey(normalizeApiKeyInput(String(key)), params.agentDir);
await setOpencodeZenApiKey(normalizeApiKeyInput(String(key ?? "")), params.agentDir);
}
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "opencode:default",
@@ -969,7 +969,7 @@ export async function applyAuthChoiceApiProviders(
message: "Enter Together AI API key",
validate: validateApiKeyInput,
});
await setTogetherApiKey(normalizeApiKeyInput(String(key)), params.agentDir);
await setTogetherApiKey(normalizeApiKeyInput(String(key ?? "")), params.agentDir);
}
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "together:default",
@@ -1025,7 +1025,7 @@ export async function applyAuthChoiceApiProviders(
message: "Enter QIANFAN API key",
validate: validateApiKeyInput,
});
setQianfanApiKey(normalizeApiKeyInput(String(key)), params.agentDir);
setQianfanApiKey(normalizeApiKeyInput(String(key ?? "")), params.agentDir);
}
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "qianfan:default",