feat(security): block credentials from reaching the LLM via prompt secret scan
All checks were successful
Deploy to Production / deploy (push) Successful in 1m20s

Prompts were sent to the model with no secret scan, so a pasted API key would leak to the LLM. Added findSecretInPrompt in @bmm/types (tight provider-key patterns: Anthropic/OpenAI/GitHub/AWS/Google/Slack/Stripe/JWT/private-key) shared by both sides. The web wizard blocks before sending with a clear message; the API preview and preview-stream endpoints reject with secret_in_prompt as the hard guarantee. Credential VALUES already never touched the model - they are entered in the separate encrypted step 2; this closes the remaining leak path where a user pastes a key into the prompt itself.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marco Sadjadi
2026-05-31 19:52:16 +02:00
parent ee4713f82c
commit be02600759
3 changed files with 59 additions and 0 deletions

View File

@@ -212,3 +212,30 @@ export const InstallTarget = z.enum([
'raw-url',
]);
export type InstallTarget = z.infer<typeof InstallTarget>;
// ---- Prompt secret guard ----
// Unambiguous provider key/token shapes. Used to reject a prompt that contains
// a real credential BEFORE it is sent to the LLM — keys must never reach the
// model. Kept tight (provider prefixes / structural markers) so normal prompts
// don't trip it. Shared by the API preview endpoints and the web wizard.
const PROMPT_SECRET_PATTERNS: ReadonlyArray<{ name: string; re: RegExp }> = [
{ name: 'an Anthropic key', re: /\bsk-ant-[A-Za-z0-9_-]{20,}/ },
{ name: 'an OpenAI project key', re: /\bsk-proj-[A-Za-z0-9_-]{20,}/ },
{ name: 'an OpenAI key', re: /\bsk-[A-Za-z0-9]{32,}\b/ },
{ name: 'a GitHub token', re: /\bgh[posru]_[A-Za-z0-9]{30,}\b/ },
{ name: 'an AWS access key', re: /\bAKIA[0-9A-Z]{16}\b/ },
{ name: 'a Google API key', re: /\bAIza[0-9A-Za-z_-]{35}\b/ },
{ name: 'a Slack token', re: /\bxox[baprs]-[A-Za-z0-9-]{10,}/ },
{ name: 'a Stripe secret key', re: /\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{20,}/ },
{ name: 'a Stripe publishable key', re: /\bpk_(?:live|test)_[A-Za-z0-9]{20,}/ },
{ name: 'a JWT / bearer token', re: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/ },
{ name: 'a private key block', re: /-----BEGIN (?:RSA |EC |OPENSSH |PGP )?PRIVATE KEY-----/ },
];
/** First secret-like token found in the text, else null. */
export function findSecretInPrompt(text: string): string | null {
for (const { name, re } of PROMPT_SECRET_PATTERNS) {
if (re.test(text)) return name;
}
return null;
}