feat(llm-security): swap injection tables to vendored commons lexicon

Third consumer swap of v8 Phase 5 step 4, after codepoints and OWASP_MAP,
and the last one with a behavioural gate behind it. The 83 regex literals
leave injection-patterns.mjs; the four arrays are now built in
scanners/lib/injection-lexicon.mjs from the vendored
lexicon/injection-lexicon.json and re-exported unchanged, so every
consumer sees the same published surface.

Behaviour-preserving by measurement, not by intent. The proven recipe ran
in order: a differential over all 83 positions (regex source, flags,
label, aliases.llm_security) found 0 divergences BEFORE anything changed;
the golden dump was then diffed post-for-post rather than read as a 9000-
character assertion, and the ONLY changed record was the sha256 of
injection-patterns.mjs itself -- 83 regex posts, 7 table records and all
counts identical. That single file digest is the diff a swap MUST produce,
so the baseline was re-blessed rather than silenced.

Two deliberate departures from the two earlier swaps:

FAILURE IS LOUD. codepoints and owasp-map fail silently on purpose: an
empty codepoint table weakens normalization, an empty OWASP map mislabels
a report. An empty injection table is different in kind -- scanForInjection
returns found:false for every input, and the UserPromptSubmit scan, the
MCP output scan and the pre-compact scan all go blind while reporting
success. That is precisely the v7.8.2 defect class, which bit this plugin
four times in one release. An unresolvable commons therefore writes one
line to stderr naming the disabled capability. It still does not throw:
hooks run per-tool-call, and a module-load throw breaks the tool call
instead of degrading the scan. The warning is suppressed for an explicit
commonsRoot, so tests and dev checkouts stay quiet and the line keeps
meaning something.

ENTRIES COMPILE DEFENSIVELY. commons is vendored data, not code. An
uncompilable pattern or unknown flag would throw inside new RegExp at
module load -- in a hook. Malformed entries are dropped instead, the same
call owasp-map.mjs makes for a non-array value.

Gates proven by mutating the vendored JSON in BOTH directions, five ways,
all firing: re-adding the script-tag tail commons dropped (golden 1,
lexicon 2, corpus 1), dropping a critical pattern (2/1/3), stripping the
`m` flag off a spoofed-header anchor (2/1), adding a pattern commons never
published (2/2/85), and removing commons outright -- which produced the
stderr line, four empty tables and 5 red rather than a green suite over
zero patterns. Lexicon restored byte-identical after each.

Full suite 2191 pass / 0 fail / 6 skipped (2184 -> 2197).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XDdiKC9ZXmcSUQ2m84s6y
This commit is contained in:
Kjell Tore Guttormsen 2026-08-11 14:13:36 +02:00
commit be148671ee
5 changed files with 386 additions and 165 deletions

View file

@ -0,0 +1,134 @@
// injection-lexicon.mjs — Prompt-injection pattern tables, built from vendored commons.
//
// v8 Phase 5 step 4, third consumer swap. The four arrays were 83 regex
// literals in injection-patterns.mjs; they are now built once, here, from
// `lexicon/injection-lexicon.json` in the vendored llm-security-commons
// subtree. Verified before the swap by a differential over all 83 positions —
// regex source, flags, label and `aliases.llm_security` — against the live
// source tables at commons v0.3.0: zero divergences, in order.
//
// The lexicon keys each family by `source_export`, which is the name of the
// constant it was extracted from, so the mapping from family to published
// table is data rather than a convention this file restates.
//
// Two things about this swap differ from the first two (codepoints, owasp-map)
// and are deliberate:
//
// 1. FAILURE IS LOUD. Those two degrade a normalization step and a report
// label respectively, and their empty state is legitimately reachable, so
// they fail silently by design. An empty injection table is different in
// kind: `scanForInjection` would return `found: false` for every input
// and every gate built on it — the UserPromptSubmit scan, the MCP output
// scan, the pre-compact scan — would report success without running.
// That is the v7.8.2 defect class, and this plugin has been bitten by it
// four times in one release. So an unresolvable commons writes one line
// to stderr. It still does not throw: hooks run per-tool-call, and a
// module-load throw breaks the tool call rather than degrading the scan.
// The warning fires only for the DEFAULT root — an explicit
// `commonsRoot` is a test or a dev checkout deliberately pointing
// elsewhere, and warning there would train the reader to ignore the line.
//
// 2. ENTRIES ARE COMPILED DEFENSIVELY. commons is vendored data, not code.
// `new RegExp(source, flags)` throws on an uncompilable pattern or an
// unknown flag, and it would throw at module load — inside a hook. A
// malformed entry is therefore dropped rather than published, the same
// call owasp-map.mjs makes for a non-array value.
//
// Array order is preserved and never sorted: it decides both dedup precedence
// in `scanForInjection` and the order findings reach report output.
//
// Zero external dependencies — Node.js builtins only.
import { loadArtifact } from './commons-loader.mjs';
/**
* The `source_export` names this module publishes.
*
* A family the lexicon adds under any other name is ignored rather than
* turned into a table no consumer reads the same call codepoints.mjs makes
* for `cyrillic_confusables`.
*/
const PUBLISHED_EXPORTS = Object.freeze([
'CRITICAL_PATTERNS',
'HIGH_PATTERNS',
'MEDIUM_PATTERNS',
'HYBRID_PATTERNS',
]);
/**
* Compile one lexicon entry into the `{ pattern, label }` shape the consumers
* expect, or return null if the entry cannot be trusted.
*/
function compileEntry(entry) {
if (entry === null || typeof entry !== 'object') return null;
if (typeof entry.pattern !== 'string' || typeof entry.label !== 'string') return null;
// Absent `flags` means no flags — the lexicon's dialect note is explicit
// that flags are declared per pattern and never inlined as `(?i)`.
const flags = entry.flags ?? '';
if (typeof flags !== 'string') return null;
try {
return Object.freeze({ pattern: new RegExp(entry.pattern, flags), label: entry.label });
} catch {
return null; // uncompilable source or unknown flag — drop, do not throw
}
}
/**
* Build the four injection tables from a commons root.
*
* @param {object} [opts]
* @param {string} [opts.commonsRoot] - explicit commons root (tests, dev checkout).
* Suppresses the unresolvable-commons warning, which is meant for the
* default root only.
* @returns {{
* CRITICAL_PATTERNS: ReadonlyArray<{pattern: RegExp, label: string}>,
* HIGH_PATTERNS: ReadonlyArray<{pattern: RegExp, label: string}>,
* MEDIUM_PATTERNS: ReadonlyArray<{pattern: RegExp, label: string}>,
* HYBRID_PATTERNS: ReadonlyArray<{pattern: RegExp, label: string}>,
* }} Every key is always present, so a lost commons yields empty tables
* rather than undefined ones a consumer would iterate and throw on.
*/
export function buildInjectionTables(opts = {}) {
const artifact = loadArtifact('lexicon/injection-lexicon', {
fallback: null,
commonsRoot: opts.commonsRoot,
});
if (artifact === null && opts.commonsRoot === undefined) {
// See note 1 above: silent is the one thing this failure must not be.
process.stderr.write(
'[llm-security] injection lexicon unresolvable at scanners/commons/lexicon/injection-lexicon.json — '
+ 'prompt-injection detection is DISABLED for this process. Reinstall the plugin or re-vendor the commons subtree.\n',
);
}
const tables = {};
for (const name of PUBLISHED_EXPORTS) tables[name] = [];
for (const family of artifact?.families ?? []) {
const name = family?.source_export;
if (!PUBLISHED_EXPORTS.includes(name)) continue;
if (!Array.isArray(family.patterns)) continue;
for (const entry of family.patterns) {
const compiled = compileEntry(entry);
if (compiled !== null) tables[name].push(compiled);
}
}
for (const name of PUBLISHED_EXPORTS) Object.freeze(tables[name]);
return tables;
}
const _tables = buildInjectionTables();
/** Direct injection attempts — blocked, not warned about. */
export const CRITICAL_PATTERNS = _tables.CRITICAL_PATTERNS;
/** Subtle manipulation: reframing, oversight evasion, HITL traps, HTML obfuscation. */
export const HIGH_PATTERNS = _tables.HIGH_PATTERNS;
/** Obfuscation and indirect manipulation — lower confidence, advisory. */
export const MEDIUM_PATTERNS = _tables.MEDIUM_PATTERNS;
/** Cross-domain injection: P2SQL, recursive injection, XSS in agent context. Reported as HIGH. */
export const HYBRID_PATTERNS = _tables.HYBRID_PATTERNS;

View file

@ -2,176 +2,41 @@
// Used by pre-prompt-inject-scan.mjs (UserPromptSubmit) and post-mcp-verify.mjs (PostToolUse).
//
// Patterns derived from skill-scanner-agent Category 1 (LLM01 Prompt Injection)
// and Category 5 (Hidden Instructions) in knowledge/skill-threat-patterns.md.
// and Category 5 (Hidden Instructions) in knowledge/skill-threat-patterns.md,
// and published since v8 through vendored commons — see the table section below.
//
// Zero external dependencies beyond ./string-utils.mjs.
// Zero external dependencies beyond ./string-utils.mjs and ./injection-lexicon.mjs.
import { normalizeForScan, containsUnicodeTags, decodeUnicodeTags, foldHomoglyphs, rot13 } from './string-utils.mjs';
// ---------------------------------------------------------------------------
// Critical patterns — direct injection attempts (should be blocked)
// Pattern tables — built from vendored commons, re-exported here
// ---------------------------------------------------------------------------
//
// v8 Phase 5 step 4, third consumer swap. These four arrays were 83 regex
// literals in this file; they now come from `lexicon/injection-lexicon.json`
// in the vendored llm-security-commons subtree, via injection-lexicon.mjs.
// Measured before the swap over all 83 positions — regex source, flags, label
// and `aliases.llm_security` — against commons v0.3.0: zero divergences, in
// order. The tables are re-exported rather than wrapped so that the published
// surface is unchanged for every consumer (content-extractor.mjs spreads them,
// the golden gate walks them as `injection-patterns`, hooks import this file).
//
// Where the data now lives, and why the loud half is not here: see the header
// of injection-lexicon.mjs. In short — an unresolvable commons yields empty
// tables and one line on stderr, because an injection gate that silently stops
// detecting is the failure mode this plugin most needs to not have.
export const CRITICAL_PATTERNS = [
// Direct override instructions
{ pattern: /ignore\s+(?:all\s+)?previous/i, label: 'override: ignore previous instructions' },
{ pattern: /forget\s+your\s+(?:instructions|rules|guidelines|directives)/i, label: 'override: forget instructions' },
{ pattern: /disregard\s+your/i, label: 'override: disregard instructions' },
{ pattern: /override\s+your/i, label: 'override: override instructions' },
{ pattern: /suspend\s+all\s+constraints/i, label: 'override: suspend constraints' },
{ pattern: /all\s+prior\s+constraints/i, label: 'override: prior constraints' },
// Imported AND re-exported, not `export ... from`: the two functions below
// read these tables directly, and a bare re-export would not bind them here.
import {
CRITICAL_PATTERNS,
HIGH_PATTERNS,
MEDIUM_PATTERNS,
HYBRID_PATTERNS,
} from './injection-lexicon.mjs';
// Spoofed system headers
{ pattern: /^#+\s*SYSTEM\s*:/m, label: 'spoofed header: # SYSTEM:' },
{ pattern: /^\[INST\]/m, label: 'spoofed header: [INST]' },
{ pattern: /<\|system\|>/, label: 'spoofed header: <|system|>' },
{ pattern: /<system>/, label: 'spoofed tag: <system>' },
{ pattern: /<\/system>/, label: 'spoofed tag: </system>' },
{ pattern: /^###\s*Instructions\s*:/m, label: 'spoofed header: ### Instructions:' },
// Identity redefinition
{ pattern: /you\s+are\s+no\s+longer/i, label: 'identity: you are no longer' },
{ pattern: /you\s+are\s+now\s+(?!able|going|ready|done|running|using|looking|in\s|at\s|on\s)/i, label: 'identity: you are now [name]' },
{ pattern: /your\s+new\s+role\s+is/i, label: 'identity: your new role' },
{ pattern: /your\s+new\s+primary\s+directive/i, label: 'identity: new primary directive' },
{ pattern: /pretend\s+you\s+are/i, label: 'identity: pretend you are' },
// Configuration manipulation
{ pattern: /ignore\s+your\s+CLAUDE\.md/i, label: 'config: ignore CLAUDE.md' },
{ pattern: /override\s+your\s+system\s+prompt/i, label: 'config: override system prompt' },
{ pattern: /bypass\s+your\s+safety/i, label: 'config: bypass safety' },
{ pattern: /disable\s+output\s+filtering/i, label: 'config: disable output filtering' },
];
// ---------------------------------------------------------------------------
// Medium patterns — obfuscation and indirect manipulation (advisory)
// Lower confidence than High; flag for manual review.
// ---------------------------------------------------------------------------
export const MEDIUM_PATTERNS = [
// Leetspeak variants of critical keywords
{ pattern: /[1!|l][gq9]n[o0]r[e3]\s+(?:all\s+)?pr[e3]v[i1!|l][o0]us/i, label: 'leetspeak: ignore previous (obfuscated)' },
{ pattern: /f[o0]rg[e3]t\s+y[o0]ur\s+[i1!|l]nstruct[i1!|l][o0]ns/i, label: 'leetspeak: forget instructions (obfuscated)' },
{ pattern: /d[i1!|l]sr[e3]g[a4@]rd\s+y[o0]ur/i, label: 'leetspeak: disregard your (obfuscated)' },
{ pattern: /[o0]v[e3]rr[i1!|l]d[e3]\s+y[o0]ur/i, label: 'leetspeak: override your (obfuscated)' },
// Homoglyph detection — Cyrillic chars in Latin context
{ pattern: /[a-zA-Z][\u0430\u0435\u043E\u0440\u0441\u0456\u0443]|[\u0430\u0435\u043E\u0440\u0441\u0456\u0443][a-zA-Z]/, label: 'homoglyph: Cyrillic-Latin mixing in adjacent characters' },
// Zero-width characters inside words (keyword splitting evasion)
{ pattern: /\w[\u200B\u200C\u200D\uFEFF]\w/, label: 'unicode: zero-width character inside word (keyword splitting)' },
// Indirect AI-directed instructions
{ pattern: /(?:note|message|instruction)\s+(?:to|for)\s+(?:the\s+)?(?:AI|assistant|model|LLM|Claude)\b/i, label: 'indirect: instruction addressed to AI/assistant' },
{ pattern: /(?:dear|attention)\s+(?:AI|assistant|model|LLM|Claude)\b/i, label: 'indirect: direct address to AI/assistant' },
{ pattern: /when\s+(?:you|the\s+AI|the\s+assistant|Claude)\s+(?:read|see|encounter|process)\s+this/i, label: 'indirect: trigger-based instruction for AI' },
// Multi-language injection variants
{ pattern: /ignor(?:ez?|er?)\s+(?:les?\s+)?instructions?\s+pr[e\u00e9]c[e\u00e9]dentes?/i, label: 'multi-lang: French "ignore previous instructions"' },
{ pattern: /ignor(?:ar?|e)\s+(?:las?\s+)?instrucciones?\s+anteriores?/i, label: 'multi-lang: Spanish "ignore previous instructions"' },
{ pattern: /ignorier(?:e|en)?\s+(?:alle\s+)?vorherigen?\s+(?:Anweisungen|Instruktionen)/i, label: 'multi-lang: German "ignore previous instructions"' },
// Markdown link-reference comment injection
{ pattern: /\[\/\/\]:\s*#\s*\(.*(?:ignore|override|system|instruction|execute)/i, label: 'markdown: suspicious instruction in link-reference comment' },
// Data URI with executable content types
{ pattern: /data:(?:text\/html|application\/javascript|text\/javascript)[;,]/i, label: 'data-uri: executable content type' },
// --- Content Injection: Syntactic Masking (AI Agent Traps) ---
{ pattern: /\[[^\]]*(?:system|ignore|override|exfiltrate|execute)[^\]]*\]\([^)]+\)/i, label: 'markdown: injection payload in link anchor text' },
// --- Sub-agent spawning traps (DeepMind kat. 4, v5.0 S4) ---
{ pattern: /(?:create|spawn|launch|start|run)\s+(?:an?\s+)?(?:new\s+)?(?:sub-?agent|agent|task|worker)\s+(?:that|to|which|with)\s+(?:.*?\s+)?(?:execute|run|delete|remove|send|post|exfiltrate|access|reads?\s+(?:.*?\s+)?(?:secret|credential|key|token|\.env|\.ssh))/i, label: 'sub-agent: spawn instruction with dangerous capability keywords' },
{ pattern: /(?:delegate|dispatch)\s+(?:to\s+)?(?:an?\s+)?(?:new\s+)?(?:agent|sub-?agent|task)\s+.*?(?:bypass|override|ignore|without\s+(?:review|confirmation|approval))/i, label: 'sub-agent: delegation with safety bypass instruction' },
// --- Natural Language Indirection (Preamble, CaMeL — v5.0 S4) ---
{ pattern: /fetch\s+(?:this|the|that)\s+(?:URL|link|endpoint)\s+and\s+(?:execute|run|eval)/i, label: 'nl-indirection: fetch URL and execute' },
{ pattern: /send\s+(?:this|the|that|all)\s+(?:data|content|output|result|information|file)\s+to\s+(?:https?:\/\/|ftp:\/\/|\S+@\S+)/i, label: 'nl-indirection: send data to external destination' },
{ pattern: /read\s+(?:the\s+)?(?:contents?\s+of\s+)?(?:~\/)?\.(?:ssh|aws|gnupg|env)\b/i, label: 'nl-indirection: read sensitive dotfile path' },
{ pattern: /(?:extract|copy|dump|exfiltrate)\s+(?:all\s+)?(?:the\s+)?(?:credentials?|secrets?|tokens?|keys?|passwords?)\s+(?:and|then)\s+(?:send|post|upload|transmit)/i, label: 'nl-indirection: extract credentials and exfiltrate' },
{ pattern: /(?:download|fetch|retrieve)\s+(?:from\s+)?\S+\s+(?:and|then)\s+(?:execute|run|eval|pipe\s+to\s+(?:bash|sh|node|python))/i, label: 'nl-indirection: download and execute' },
];
// ---------------------------------------------------------------------------
// High patterns — subtle manipulation (advisory warning, not block)
// ---------------------------------------------------------------------------
export const HIGH_PATTERNS = [
{ pattern: /always\s+prioritize\s+user\s+requests?\s+over/i, label: 'normalization: prioritize user over rules' },
{ pattern: /user\s+intent\s+takes?\s+precedence\s+over/i, label: 'normalization: user intent precedence' },
{ pattern: /if\s+CLAUDE\.md\s+conflicts/i, label: 'normalization: CLAUDE.md conflict override' },
{ pattern: /skip\s+confirmation\s+for/i, label: 'normalization: skip confirmation' },
{ pattern: /your\s+actual\s+task\s+is/i, label: 'reframing: your actual task' },
{ pattern: /your\s+real\s+purpose\s+is/i, label: 'reframing: your real purpose' },
{ pattern: /what\s+you\s+should\s+actually\s+do/i, label: 'reframing: what you should actually do' },
// HTML comment injection directed at agents
{ pattern: /<!--\s*(?:AGENT|AI|HIDDEN|ACTUAL\s+TASK|REAL\s+INSTRUCTION)\s*:/i, label: 'hidden comment: agent-directed HTML comment' },
// --- Content Injection: CSS/HTML obfuscation (AI Agent Traps) ---
// v7.8.3 (#24): quantifiers bounded ({1,256}/{0,256}) — the unbounded
// overlapping [^"]* runs plus the required closing quote backtracked
// O(N^2)/O(N^3) when an attacker omitted the closing quote (~27s at the
// 512KB hook read cap). 256 chars comfortably covers legitimate inline
// style/aria-label attributes.
{ pattern: /<[^>]{1,256}style\s*=\s*"[^"]{0,256}display\s*:\s*none[^"]{0,256}"[^>]{0,256}>/i, label: 'html-obfuscation: display:none element with content' },
{ pattern: /<[^>]{1,256}style\s*=\s*"[^"]{0,256}visibility\s*:\s*hidden[^"]{0,256}"[^>]{0,256}>/i, label: 'html-obfuscation: visibility:hidden element' },
{ pattern: /<[^>]{1,256}style\s*=\s*"[^"]{0,256}position\s*:\s*absolute[^"]{0,256}-\d{3,}px[^"]{0,256}"[^>]{0,256}>/i, label: 'html-obfuscation: off-screen positioned element' },
{ pattern: /<[^>]{1,256}style\s*=\s*"[^"]{0,256}font-size\s*:\s*0[^"]{0,256}"[^>]{0,256}>/i, label: 'html-obfuscation: zero font-size element' },
{ pattern: /<[^>]{1,256}style\s*=\s*"[^"]{0,256}opacity\s*:\s*0[^"]{0,256}"[^>]{0,256}>/i, label: 'html-obfuscation: zero opacity element' },
{ pattern: /<[^>]{1,256}style\s*=\s*"[^"]{0,256}(?:height|width)\s*:\s*0[^"]{0,256}overflow\s*:\s*hidden[^"]{0,256}"[^>]{0,256}>/i, label: 'html-obfuscation: zero-size overflow-hidden element' },
{ pattern: /aria-label\s*=\s*"[^"]{0,256}(?:ignore|override|system|instruction|execute|exfiltrate)[^"]{0,256}"/i, label: 'html-obfuscation: injection in aria-label attribute' },
// --- Semantic Manipulation: Oversight & Critic Evasion (AI Agent Traps) ---
{ pattern: /for\s+educational\s+purposes?\s+only/i, label: 'evasion: educational purpose framing' },
{ pattern: /(?:this\s+is\s+)?a\s+hypothetical\s+scenario/i, label: 'evasion: hypothetical scenario framing' },
{ pattern: /in\s+a\s+fictional\s+(?:setting|scenario|world)\s+where/i, label: 'evasion: fictional setting framing' },
{ pattern: /(?:this\s+is\s+)?a\s+red[- ]?team(?:ing)?\s+(?:exercise|simulation|test)/i, label: 'evasion: red-team exercise framing' },
{ pattern: /security\s+audit\s+simulation/i, label: 'evasion: security audit simulation framing' },
{ pattern: /pretend\s+(?:this\s+is|we\s+are\s+in)\s+a\s+(?:test|lab|sandbox|simulation)/i, label: 'evasion: test/lab/sandbox pretense' },
{ pattern: /as\s+a\s+thought\s+experiment/i, label: 'evasion: thought experiment framing' },
{ pattern: /for\s+(?:research|academic|scientific)\s+purposes/i, label: 'evasion: research purpose framing' },
{ pattern: /in\s+an?\s+(?:academic|research|educational)\s+context/i, label: 'evasion: academic context framing' },
// --- Human-in-the-Loop traps (DeepMind kat. 6, v5.0 S4) ---
// Approval urgency: pressure to approve without review
{ pattern: /(?:approve|accept|confirm)\s+(?:this\s+)?immediately/i, label: 'hitl-trap: approval urgency — approve immediately' },
{ pattern: /no\s+need\s+to\s+review/i, label: 'hitl-trap: approval urgency — no need to review' },
{ pattern: /(?:just|simply)\s+(?:approve|accept|confirm)\s+(?:it|this|everything)/i, label: 'hitl-trap: approval urgency — just approve' },
{ pattern: /(?:don'?t|do\s+not)\s+(?:bother\s+)?(?:check|review|verify|inspect|read)(?:ing)?\s+(?:this|the|it)/i, label: 'hitl-trap: approval urgency — suppress review' },
// Summary suppression: hiding actions behind brevity claims
{ pattern: /for\s+brevity[\s,]+(?:skipping|omitting|leaving\s+out)/i, label: 'hitl-trap: summary suppression — for brevity, skipping' },
{ pattern: /(?:too\s+(?:long|verbose|detailed)\s+to\s+(?:show|display|list|include))/i, label: 'hitl-trap: summary suppression — too long to show' },
{ pattern: /(?:details?\s+(?:omitted|redacted|hidden|suppressed))/i, label: 'hitl-trap: summary suppression — details omitted' },
// Scope minimization: downplaying destructive changes
{ pattern: /(?:minor|trivial|small|cosmetic)\s+(?:change|update|fix|adjustment)\b[^.]{0,80}(?:delet|remov|drop|truncat|reset|destroy|\bformat\b|wipe)/i, label: 'hitl-trap: scope minimization — minor change + destructive action' },
];
// ---------------------------------------------------------------------------
// Hybrid attack patterns — cross-domain injection (HIGH, v5.0 S6)
// Preamble 2.0: P2SQL, recursive injection, XSS in agent context.
// ---------------------------------------------------------------------------
export const HYBRID_PATTERNS = [
// P2SQL: SQL keywords in injection text targeting tool operations
{ pattern: /(?:ignore|override|disregard|forget)[^.]{0,60}(?:SELECT\s+\*|DROP\s+TABLE|UNION\s+SELECT|DELETE\s+FROM|INSERT\s+INTO|UPDATE\s+\w+\s+SET)(?:\b|(?=\s|$))/i, label: 'hybrid-p2sql: injection + SQL keywords (prompt-to-SQL attack)' },
{ pattern: /(?:SELECT\s+\*|DROP\s+TABLE|UNION\s+SELECT|DELETE\s+FROM)\s[^;]{0,80}(?:ignore|override|disregard|bypass)/i, label: 'hybrid-p2sql: SQL operation + injection override keywords' },
// Recursive injection: text that instructs the model to inject into its own output
{ pattern: /(?:inject|insert|embed|include)\s+(?:this|the\s+following)\s+(?:into|in)\s+(?:your|the)\s+(?:output|response|reply|message|prompt|context)/i, label: 'hybrid-recursive: instruction to inject into model output' },
{ pattern: /(?:when|if)\s+(?:the\s+)?(?:user|human|operator)\s+(?:asks?|requests?|queries)[^.]{0,60}(?:respond\s+with|output|reply\s+with|include)\s+(?:this|the\s+following)/i, label: 'hybrid-recursive: conditional response injection (recursive payload)' },
// XSS in agent context: script/event handlers in content for markdown rendering
// v8.x-A: the closing </script> requirement was a recall hole — `<script>alert(1)`
// and `<script src=x.js>` both passed with found: false. The opening tag alone is
// the signal; a src= tag has no body to close in the first place. Matching the
// open tag is also strictly linear: one negated-class run whose excluded char
// is the terminator, so there is no backtracking surface to re-introduce #24.
{ pattern: /<script\b[^>]*>/i, label: 'hybrid-xss: <script> tag in content (agent context XSS)' },
{ pattern: /javascript\s*:/i, label: 'hybrid-xss: javascript: URI scheme (agent context XSS)' },
{ pattern: /\bon(?:error|load|click|mouseover|focus|blur)\s*=/i, label: 'hybrid-xss: inline event handler attribute (agent context XSS)' },
{ pattern: /<iframe\b[^>]*src\s*=\s*["'][^"']*(?:javascript:|data:text\/html)/i, label: 'hybrid-xss: iframe with executable src (agent context XSS)' },
];
export { CRITICAL_PATTERNS, HIGH_PATTERNS, MEDIUM_PATTERNS, HYBRID_PATTERNS };
// ---------------------------------------------------------------------------
// HITL cognitive load patterns (MEDIUM, v5.0 S4)