fix(security): eliminate shell from Claude CLI keychain refresh

This commit is contained in:
Peter Steinberger
2026-02-14 17:24:29 +01:00
parent d583782ee3
commit 66d7178f2d
3 changed files with 351 additions and 11 deletions

View File

@@ -382,13 +382,13 @@ export function readClaudeCliCredentialsCached(options?: {
export function writeClaudeCliKeychainCredentials(
newCredentials: OAuthCredentials,
options?: { execSync?: ExecSyncFn; execFileSync?: ExecFileSyncFn },
options?: { execFileSync?: ExecFileSyncFn },
): boolean {
const execSyncImpl = options?.execSync ?? execSync;
const execFileSyncImpl = options?.execFileSync ?? execFileSync;
try {
const existingResult = execSyncImpl(
`security find-generic-password -s "${CLAUDE_CLI_KEYCHAIN_SERVICE}" -w 2>/dev/null`,
const existingResult = execFileSyncImpl(
"security",
["find-generic-password", "-s", CLAUDE_CLI_KEYCHAIN_SERVICE, "-w"],
{ encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] },
);
@@ -409,13 +409,20 @@ export function writeClaudeCliKeychainCredentials(
// Use execFileSync to avoid shell interpretation of user-controlled token values.
// This prevents command injection via $() or backtick expansion in OAuth tokens.
execFileSyncImpl("security", [
"add-generic-password",
"-U",
"-s", CLAUDE_CLI_KEYCHAIN_SERVICE,
"-a", CLAUDE_CLI_KEYCHAIN_ACCOUNT,
"-w", newValue,
], { encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
execFileSyncImpl(
"security",
[
"add-generic-password",
"-U",
"-s",
CLAUDE_CLI_KEYCHAIN_SERVICE,
"-a",
CLAUDE_CLI_KEYCHAIN_ACCOUNT,
"-w",
newValue,
],
{ encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] },
);
log.info("wrote refreshed credentials to claude cli keychain", {
expires: new Date(newCredentials.expires).toISOString(),