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

@@ -28,6 +28,7 @@ import {
IterateServerInput,
PreviewInput,
type SpecEdit,
findSecretInPrompt,
} from '@bmm/types';
import type { FastifyInstance } from 'fastify';
import { z } from 'zod';
@@ -62,6 +63,16 @@ export async function serverRoutes(app: FastifyInstance): Promise<void> {
if (!parsed.success) {
return reply.code(400).send({ error: 'invalid_input', issues: parsed.error.flatten() });
}
// Never let a credential reach the LLM. Reject prompts that contain a
// real-looking key/token before the model call. (Values belong in the
// separate encrypted credential fields, not the prompt.)
const leakedSecret = findSecretInPrompt(parsed.data.prompt);
if (leakedSecret) {
return reply.code(400).send({
error: 'secret_in_prompt',
detail: `Your prompt looks like it contains ${leakedSecret}. Remove it — API keys must never go in the prompt (it is sent to the AI model). You will add credentials in their own encrypted fields after the spec is generated.`,
});
}
const billing = await getOrgBilling(user.orgId);
if (billing.suspended) {
@@ -190,6 +201,16 @@ export async function serverRoutes(app: FastifyInstance): Promise<void> {
if (!parsed.success) {
return reply.code(400).send({ error: 'invalid_input', issues: parsed.error.flatten() });
}
// Never let a credential reach the LLM. Reject prompts that contain a
// real-looking key/token before the model call. (Values belong in the
// separate encrypted credential fields, not the prompt.)
const leakedSecret = findSecretInPrompt(parsed.data.prompt);
if (leakedSecret) {
return reply.code(400).send({
error: 'secret_in_prompt',
detail: `Your prompt looks like it contains ${leakedSecret}. Remove it — API keys must never go in the prompt (it is sent to the AI model). You will add credentials in their own encrypted fields after the spec is generated.`,
});
}
const billing = await getOrgBilling(user.orgId);
if (billing.suspended) {