UNPKG

llm-inject-scan

Version:

A tiny, fast library that scans user prompts for risky patterns before they reach your LLM model. It flags likely prompt-injection attempts so you can block, review, or route them differently—without making a model call.

350 lines (343 loc) • 9.46 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { FlaggedCategory: () => FlaggedCategory, createPromptValidator: () => createPromptValidator }); module.exports = __toCommonJS(index_exports); // utils/normalizeString.ts var HOMOGLYPH_MAP = { // Latin variants and diacritics are handled by NFKD + diacritics removal below // Common confusables (subset) \u0131: "i", // Latin dotless i \u0130: "i", \u00CC: "i", \u00CD: "i", \u00CE: "i", \u00CF: "i", \u00EC: "i", \u00ED: "i", \u00EE: "i", \u00EF: "i", \u0100: "a", \u00C1: "a", \u00C0: "a", \u00C2: "a", \u00C3: "a", \u00C4: "a", \u00C5: "a", \u0101: "a", \u00E1: "a", \u00E0: "a", \u00E2: "a", \u00E3: "a", \u00E4: "a", \u00E5: "a", \u0112: "e", \u00C9: "e", \u00C8: "e", \u00CA: "e", \u00CB: "e", \u0113: "e", \u00E9: "e", \u00E8: "e", \u00EA: "e", \u00EB: "e", \u014C: "o", \u00D3: "o", \u00D2: "o", \u00D4: "o", \u00D5: "o", \u00D6: "o", \u014D: "o", \u00F3: "o", \u00F2: "o", \u00F4: "o", \u00F5: "o", \u00F6: "o", \u016A: "u", \u00DA: "u", \u00D9: "u", \u00DB: "u", \u00DC: "u", \u016B: "u", \u00FA: "u", \u00F9: "u", \u00FB: "u", \u00FC: "u", // Cyrillic letters that resemble Latin (subset) \u0410: "a", \u0430: "a", \u0412: "b", \u0432: "b", \u0415: "e", \u0435: "e", \u041A: "k", \u043A: "k", \u041C: "m", \u043C: "m", \u041D: "h", \u043D: "h", \u041E: "o", \u043E: "o", \u0420: "p", \u0440: "p", \u0421: "c", \u0441: "c", \u0422: "t", \u0442: "t", \u0425: "x", \u0445: "x", \u0406: "i", \u0456: "i", \u0407: "i", \u0457: "i", \u0408: "j", \u0458: "j", \u0405: "s", \u0455: "s", // Greek confusables (subset) \u0391: "a", \u03B1: "a", \u0392: "b", \u03B2: "b", \u0395: "e", \u03B5: "e", \u0396: "z", \u03B6: "z", \u0397: "n", \u03B7: "n", // rough mapping \u0399: "i", \u03B9: "i", \u039A: "k", \u03BA: "k", \u039C: "m", \u03BC: "m", \u039D: "n", \u03BD: "v", \u039F: "o", \u03BF: "o", \u03A1: "p", \u03C1: "p", \u03A4: "t", \u03C4: "t", \u03A5: "y", \u03C5: "y", \u03A7: "x", \u03C7: "x", // Common leetspeak "0": "o", "1": "l", // map to l (covers the example "l with 1") "3": "e", "4": "a", "5": "s", "7": "t" }; var normalizeString = (input) => { let s = input.normalize("NFKD").replace(/[\u0300-\u036f]/g, ""); s = s.toLowerCase(); s = Array.from(s).map((ch) => { var _a; return (_a = HOMOGLYPH_MAP[ch]) != null ? _a : ch; }).join(""); s = s.replace(/[^a-z0-9]+/g, ""); return s; }; // utils/rules.ts var MIN_MATCH_THRESHOLD = 0.65; var containsRuleIn = (normalizedHaystack, rule) => { const tokens = rule.split("|").map((t) => normalizeString(t)).filter((t) => t.length > 0); if (tokens.length === 0) return false; if (tokens.length === 1) { return normalizedHaystack.includes(tokens[0]); } const requiredMatches = Math.ceil(tokens.length * MIN_MATCH_THRESHOLD); let matchedCount = 0; for (const token of tokens) { if (normalizedHaystack.includes(token)) { matchedCount += 1; if (matchedCount >= requiredMatches) return true; } } return false; }; var anyRuleMatches = (normalized, reversedNormalized, rules) => { return rules.some((rule) => containsRuleIn(normalized, rule) || containsRuleIn(reversedNormalized, rule)); }; // utils/fuzzyMatching.ts var equalsWithOneAdjacentTranspositionOrEqual = (a, b) => { if (a === b) return true; if (a.length !== b.length) return false; let firstDiff = -1; for (let i = 0; i < a.length; i++) { if (a[i] !== b[i]) { firstDiff = i; break; } } if (firstDiff < 0) return true; const j = firstDiff + 1; if (j >= a.length) return false; if (a[firstDiff] === b[j] && a[j] === b[firstDiff]) { for (let k = j + 1; k < a.length; k++) { if (a[k] !== b[k]) return false; } return true; } return false; }; var fuzzyIncludesTransposition = (normalizedHaystack, term) => { const n = normalizedHaystack.length; const m = term.length; if (m === 0 || n < m) return false; for (let i = 0; i <= n - m; i++) { const sub = normalizedHaystack.slice(i, i + m); if (equalsWithOneAdjacentTranspositionOrEqual(sub, term)) return true; } return false; }; // utils/detectors.ts var isBase64Like = (s) => /[A-Za-z0-9+/=]{20,}/.test(s); var containsUrl = (s) => /https?:\/\/\S+/i.test(s); var startsWithRoleLabel = (s) => /^(\s*)(system|assistant|boss)\s*:/i.test(s); var containsPercentEncodedText = (s) => /(?:%[0-9A-Fa-f]{2}){3,}/.test(s); // src/index.ts var FlaggedCategory = /* @__PURE__ */ ((FlaggedCategory2) => { FlaggedCategory2["Jailbreak"] = "jailbreak"; FlaggedCategory2["Evasion"] = "evasion"; FlaggedCategory2["PromptLeak"] = "promptLeak"; FlaggedCategory2["Indirect"] = "indirect"; FlaggedCategory2["RoleContext"] = "roleContext"; return FlaggedCategory2; })(FlaggedCategory || {}); var DEFAULT_LOCALE = "en-US"; var injectionRulesEnUS = { ["jailbreak" /* Jailbreak */]: [ // Include both with and without "all" to avoid optional token syntax "ignore|previous|instructions", "ignore|all|previous|instructions", "you|are|now|in|developer|mode", "pretend|youre", "forget|everything", "tell|system|prompt|security", "tell|password", "spell|backward", "replace|number|letter|character", "delete|everything|records", "disregard|previous|instruction|command", "disregard|system|prompt", "provide|detail|system|security", "let|f(x)|f(y)", "do|anything|now", "replace|response", "add|answer", "without|restrictions" ], ["evasion" /* Evasion */]: [ // direct terms (single-token rules) "ignore", "bypass", "reveal", "create", "malware", "code", "skip", "opposite" ], ["promptLeak" /* PromptLeak */]: [ "reveal|your|internal|prompt", "reveal|your|system|prompt", "what|were|your|instructions", "repeat|the|text|above|starting|with|you|are", "your|system|prompt", "creators|told|you|never|to|reveal" ], ["indirect" /* Indirect */]: [ // Rule-based not needed (handled by URL detector), keep placeholder for structure ], ["roleContext" /* RoleContext */]: [ "system|override", "earth|is|flat", "the|next|sentence|is|true|the|previous|sentence|is|false" ] }; var detectMaliciousPromptV2 = (rulesByCategory, options, userInput) => { const flaggedCategories = []; const { disableBase64Check = false, disableUrlCheck = false, disableUrlEncodingCheck = false } = options; const normalized = normalizeString(userInput); const reversedNormalized = Array.from(normalized).reverse().join(""); for (const category of Object.keys(rulesByCategory)) { const rules = rulesByCategory[category]; if (rules.length === 0) continue; if (anyRuleMatches(normalized, reversedNormalized, rules)) { flaggedCategories.push(category); } } if (!disableBase64Check && isBase64Like(userInput)) { if (!flaggedCategories.includes("evasion" /* Evasion */)) { flaggedCategories.push("evasion" /* Evasion */); } } if (!disableUrlEncodingCheck && containsPercentEncodedText(userInput)) { if (!flaggedCategories.includes("evasion" /* Evasion */)) { flaggedCategories.push("evasion" /* Evasion */); } } if (!disableUrlCheck && containsUrl(userInput)) { if (!flaggedCategories.includes("indirect" /* Indirect */)) { flaggedCategories.push("indirect" /* Indirect */); } } if (startsWithRoleLabel(userInput)) { if (!flaggedCategories.includes("roleContext" /* RoleContext */)) { flaggedCategories.push("roleContext" /* RoleContext */); } } if (!flaggedCategories.includes("evasion" /* Evasion */)) { const evasionFuzzyTerms = ["ignore", "bypass", "reveal"].map((t) => normalizeString(t)); const fuzzyHit = evasionFuzzyTerms.some((term) => fuzzyIncludesTransposition(normalized, term)); if (fuzzyHit) flaggedCategories.push("evasion" /* Evasion */); } return { flags: flaggedCategories, clean: flaggedCategories.length === 0 }; }; var createPromptValidator = (options = {}) => { const dictionaries = { [DEFAULT_LOCALE]: injectionRulesEnUS }; const { locale = DEFAULT_LOCALE } = options; const injectionRules = dictionaries[locale]; return (userInput) => detectMaliciousPromptV2( injectionRules, options, userInput ); }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { FlaggedCategory, createPromptValidator });