Files
HealthLog/src/lib/insights/sanitize.ts
T
Marc-André Bombeck abb9a37672 Cover data-field injection in the red-team battery and derive the snapshot guard
The red-team corpus probed only user messages and model replies. Neither is
the channel that reaches the prompt unguarded: a lab analyte, panel or unit
name, or a medication label, none of which the account holder authors when
the row was committed from an uploaded document.

The free-text guard's file list was hardcoded and had rotted — three of its
four entries were no longer reachable from the snapshot, and it covered one
of the dozen files that actually emit free text, which is why the lab fields
went unnoticed. Derive it from the import graph instead; it now covers 75
files, with a floor assertion so a resolver break cannot silently empty it.

Deriving the list immediately surfaced a second leak: features.ts shipped a
raw medication name and dose into the snapshot medications block.

Also fix sanitizeForPrompt itself. It deleted control characters, which
welded neighbouring tokens together and destroyed the word boundary its own
injection patterns anchor on, so a payload preceded by a newline survived
because of the newline. Replace with a space and collapse before matching.
2026-07-19 04:47:53 +02:00

38 lines
1.6 KiB
TypeScript

/**
* Sanitize user-provided strings before embedding them in LLM prompts.
* Strips control characters, prompt-injection patterns, and limits length.
*
* v1.30.25 — control characters are replaced with a SPACE, not deleted, and
* the run is collapsed before the injection patterns are applied.
*
* Deleting them was self-defeating: every injection pattern below is anchored
* on a `\b` word boundary, and deletion WELDS the neighbouring tokens
* together. `"Ferritin\nSYSTEM: …"` collapsed to `"FerritinSYSTEM: …"`, where
* `\bsystem\s*:` no longer matches — so the payload survived precisely
* BECAUSE it was preceded by a newline, which is the most natural way to
* write it. Substituting a space preserves the boundary the patterns need.
* Order is load-bearing: normalise whitespace first, then strip patterns.
*/
export function sanitizeForPrompt(input: string, maxLength = 100): string {
return (
input
// Replace control characters with a space so adjacent tokens stay
// separated (see the note above — deletion defeats the `\b` anchors).
.replace(/[\x00-\x1f\x7f]/g, " ")
// Collapse the runs the substitution just created.
.replace(/\s+/g, " ")
// Remove common prompt injection patterns
.replace(/```/g, "")
.replace(/\bsystem\s*:/gi, "")
.replace(/\bassistant\s*:/gi, "")
.replace(/\buser\s*:/gi, "")
.replace(/\bignore\s+(previous|above|all)\b/gi, "")
.replace(/\bforget\s+(previous|above|all)\b/gi, "")
// Collapse again — pattern removal leaves gaps behind.
.replace(/\s+/g, " ")
// Limit length
.slice(0, maxLength)
.trim()
);
}