checkPromptInjection now NFKD-normalises, strips zero-width / combining chars, and folds common Cyrillic / Greek homoglyphs before matching. 10 documented bypass examples (fullwidth, ZWJ, ZWSP, soft-hyphen, Cyrillic е/о, combining marks, LRM, BOM) are covered by unit tests. Security docs explicitly mark the guard as defense-in-depth — real boundary is per-tool requirePermission. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
109 lines
3.4 KiB
TypeScript
109 lines
3.4 KiB
TypeScript
/**
|
|
* Prompt-injection detection for AI inputs.
|
|
*
|
|
* Defense-in-depth only — the real authorization boundary is the per-tool
|
|
* permission check (`requirePermission` on each assistant tool). This guard
|
|
* exists so deliberate injection attempts are (a) logged / alerted on and
|
|
* (b) blocked for hot-wired paths (e.g. DALL-E prompt concat) that don't
|
|
* run through tool-calls. It WILL be bypassed by a motivated attacker.
|
|
*
|
|
* Normalisation before regex:
|
|
* 1) Unicode NFKC — collapses compatibility forms (`ignore` → `ignore`).
|
|
* 2) Strip zero-width + directional control chars (ZWSP, ZWJ, LRM, RLM …).
|
|
* 3) Strip combining marks (diacritics etc.) after NFKC splits them.
|
|
* 4) Map a small set of Cyrillic / Greek homoglyphs to ASCII.
|
|
*
|
|
* EGAI 4.6.3.2 — Prompt Injection Detection
|
|
*/
|
|
|
|
const INJECTION_PATTERNS = [
|
|
/ignore\s+(all\s+)?previous\s+instructions/i,
|
|
/disregard\s+(all\s+)?prior/i,
|
|
/you\s+are\s+now\s+/i,
|
|
/forget\s+(everything|all|your)\s+(instructions|rules|guidelines)/i,
|
|
/system\s*:\s*/i,
|
|
/\[INST\]/i,
|
|
/<<SYS>>/i,
|
|
/\bDAN\b.*\bmode\b/i,
|
|
/jailbreak/i,
|
|
/bypass\s+(security|filter|restriction)/i,
|
|
/pretend\s+you\s+(are|have)\s+no\s+(rules|restrictions)/i,
|
|
/act\s+as\s+(if|though)\s+you\s+(have|are)\s+no/i,
|
|
];
|
|
|
|
// Zero-width + directional formatting characters that let an attacker insert
|
|
// `ignore` into text without the substring appearing contiguous to a regex.
|
|
const INVISIBLE_RE = /[\u200B-\u200F\u202A-\u202E\u2060-\u2064\uFEFF\u00AD]/g;
|
|
|
|
// Combining-mark block — stripped after NFKC so `n\u0303` → `n`.
|
|
const COMBINING_MARK_RE = /[\u0300-\u036F]/g;
|
|
|
|
// Minimal homoglyph fold: Cyrillic / Greek letters that render identically to
|
|
// ASCII in common fonts. Not exhaustive — a full confusables table would be
|
|
// multi-KB; this covers the realistic bypass set for our patterns.
|
|
const HOMOGLYPHS: Record<string, string> = {
|
|
"\u0430": "a",
|
|
"\u0410": "A",
|
|
"\u0435": "e",
|
|
"\u0415": "E",
|
|
"\u043E": "o",
|
|
"\u041E": "O",
|
|
"\u0440": "p",
|
|
"\u0420": "P",
|
|
"\u0441": "c",
|
|
"\u0421": "C",
|
|
"\u0445": "x",
|
|
"\u0425": "X",
|
|
"\u0443": "y",
|
|
"\u0456": "i",
|
|
"\u0406": "I",
|
|
"\u03BF": "o",
|
|
"\u0391": "A",
|
|
"\u0392": "B",
|
|
"\u0395": "E",
|
|
"\u0397": "H",
|
|
"\u0399": "I",
|
|
"\u039A": "K",
|
|
"\u039C": "M",
|
|
"\u039D": "N",
|
|
"\u039F": "O",
|
|
"\u03A1": "P",
|
|
"\u03A4": "T",
|
|
"\u03A7": "X",
|
|
"\u03A5": "Y",
|
|
"\u03A2": "Z",
|
|
};
|
|
|
|
function foldHomoglyphs(input: string): string {
|
|
let out = "";
|
|
for (const ch of input) {
|
|
out += HOMOGLYPHS[ch] ?? ch;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export function normalizeForGuard(input: string): string {
|
|
// NFKD (decomposed, compatibility) instead of NFKC so that pre-composed
|
|
// diacritics like "é" split into base + combining mark; the mark is then
|
|
// removed together with attacker-inserted padding. NFKD also handles
|
|
// compatibility forms (e.g. fullwidth letters).
|
|
const nfkd = input.normalize("NFKD");
|
|
const stripped = nfkd.replace(INVISIBLE_RE, "").replace(COMBINING_MARK_RE, "");
|
|
return foldHomoglyphs(stripped);
|
|
}
|
|
|
|
export interface PromptGuardResult {
|
|
safe: boolean;
|
|
matchedPattern?: string;
|
|
}
|
|
|
|
export function checkPromptInjection(input: string): PromptGuardResult {
|
|
const normalized = normalizeForGuard(input);
|
|
for (const pattern of INJECTION_PATTERNS) {
|
|
if (pattern.test(normalized)) {
|
|
return { safe: false, matchedPattern: pattern.source };
|
|
}
|
|
}
|
|
return { safe: true };
|
|
}
|