From e90d8406b4147c9af71cf7041f197576f12ce293 Mon Sep 17 00:00:00 2001 From: SPANISH FLU Date: Mon, 26 Jan 2026 20:21:46 +0100 Subject: [PATCH] security(audit): detect API keys in auth profile names Add comprehensive secret pattern detection for auth profile names. Introduces shared secret-patterns.ts with patterns for 40+ services including Anthropic, OpenAI, Google, GitHub, Slack, AWS, Stripe, etc. The audit now warns when profile names contain what looks like an actual API key, since profile names appear in logs, UI dropdowns, and error messages. Closes #2321 Co-Authored-By: Claude Opus 4.5 --- src/security/audit-extra.ts | 64 +++++++++++++++++++++++++++++++++++++ src/security/audit.ts | 2 ++ 2 files changed, 66 insertions(+) diff --git a/src/security/audit-extra.ts b/src/security/audit-extra.ts index 9aabb9721..44d8a3b7a 100644 --- a/src/security/audit-extra.ts +++ b/src/security/audit-extra.ts @@ -28,6 +28,7 @@ import { safeStat, } from "./audit-fs.js"; import type { ExecFn } from "./windows-acl.js"; +import { getDefaultRedactPatterns } from "../logging/redact.js"; export type SecurityAuditFinding = { checkId: string; @@ -127,6 +128,69 @@ function looksLikeEnvRef(value: string): boolean { return v.startsWith("${") && v.endsWith("}"); } +function hasHighEntropy(value: string, minLength = 40, minRatio = 0.4): boolean { + if (value.length < minLength) return false; + if (!/^[A-Za-z0-9_-]+$/.test(value)) return false; + const entropy = new Set(value).size / value.length; + return entropy > minRatio; +} + +export function collectApiKeyInProfileNameFindings(cfg: ClawdbotConfig): SecurityAuditFinding[] { + const findings: SecurityAuditFinding[] = []; + const profiles = cfg.auth?.profiles; + if (!profiles || typeof profiles !== "object") return findings; + + // Compile redact patterns for matching + const patterns = getDefaultRedactPatterns() + .map((p) => { + try { + return new RegExp(p, "i"); + } catch { + return null; + } + }) + .filter((r): r is RegExp => r !== null); + + const matches: string[] = []; + + for (const profileKey of Object.keys(profiles)) { + // Profile keys are like "provider:name" - check the name part + const parts = profileKey.split(":"); + const namePart = parts.length > 1 ? parts.slice(1).join(":") : profileKey; + + // Check against redact patterns (same patterns used for log redaction) + let matched = false; + for (const pattern of patterns) { + if (pattern.test(namePart)) { + matches.push(profileKey); + matched = true; + break; + } + } + + // Fallback: high-entropy check for unknown secret formats + if (!matched && hasHighEntropy(namePart)) { + matches.push(profileKey); + } + } + + if (matches.length > 0) { + const lines = matches.map((m) => `- "${m}"`).join("\n"); + findings.push({ + checkId: "auth.profiles.key_in_name", + severity: "warn", + title: "API key detected in auth profile name", + detail: + `The following auth profile names appear to contain API keys:\n${lines}\n` + + "Profile names are not secrets and may appear in logs, error messages, and UI dropdowns.", + remediation: + 'Rename profiles to descriptive IDs like "anthropic:personal" or "openai:work" instead of using the actual key as the name.', + }); + } + + return findings; +} + export function collectSecretsInConfigFindings(cfg: ClawdbotConfig): SecurityAuditFinding[] { const findings: SecurityAuditFinding[] = []; const password = diff --git a/src/security/audit.ts b/src/security/audit.ts index 2169f197d..b8f5df1e7 100644 --- a/src/security/audit.ts +++ b/src/security/audit.ts @@ -9,6 +9,7 @@ import { formatCliCommand } from "../cli/command-format.js"; import { buildGatewayConnectionDetails } from "../gateway/call.js"; import { probeGateway } from "../gateway/probe.js"; import { + collectApiKeyInProfileNameFindings, collectAttackSurfaceSummaryFindings, collectExposureMatrixFindings, collectHooksHardeningFindings, @@ -911,6 +912,7 @@ export async function runSecurityAudit(opts: SecurityAuditOptions): Promise