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 <noreply@anthropic.com>
This commit is contained in:
SPANISH FLU 2026-01-26 20:21:46 +01:00
parent f5c90f0e5c
commit e90d8406b4
2 changed files with 66 additions and 0 deletions

View File

@ -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 =

View File

@ -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<Secu
findings.push(...collectElevatedFindings(cfg));
findings.push(...collectHooksHardeningFindings(cfg));
findings.push(...collectSecretsInConfigFindings(cfg));
findings.push(...collectApiKeyInProfileNameFindings(cfg));
findings.push(...collectModelHygieneFindings(cfg));
findings.push(...collectSmallModelRiskFindings({ cfg, env }));
findings.push(...collectExposureMatrixFindings(cfg));