/** * 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, /<>/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 = { "\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 }; }