diff --git a/src/llm_ingestion_guard/neutralize.py b/src/llm_ingestion_guard/neutralize.py new file mode 100644 index 0000000..bf0283a --- /dev/null +++ b/src/llm_ingestion_guard/neutralize.py @@ -0,0 +1,222 @@ +"""neutralize — opt-in, pure defang of active content in model OUTPUT. + +Query-time guardrails guard the answer; this guards the *persisted artifact*. +When model output is written to a wiki, doc, or knowledge base and later rendered, +active-content constructs become an exfiltration channel: a markdown image URL is +auto-fetched the moment the page renders, leaking whatever the attacker packed +into it — with no click. This is the EchoLeak class (CVE-2025-32711). Such +carriers are neither injection strings nor high-entropy blobs, so ``lexicon`` and +``entropy`` do not see them; neutralizing them is a distinct control (OWASP +LLM05 — Improper Output Handling). + +Defang, don't delete. Each active construct is rewritten to an inert but still +human-auditable form: URLs get a non-resolvable scheme and bracketed dots +(``https://evil.com`` -> ``hxxps://evil[.]com``), and raw active HTML is escaped +so a renderer shows it as literal text instead of executing it. The visible +information survives review; only the machine-actionable affordance dies. + +Two properties are load-bearing and mirror the sanitizer: + +1. **Opt-in and separate.** Calling this function *is* the opt-in to mutate. + Detection elsewhere in the library stays report-only (design principles 3 & 4); + the report-only output gate (``output``) never rewrites. A caller that wants + findings without mutation reads ``result.report`` and discards ``result.text``. +2. **Byte-identical on clean input.** Output with no active construct is returned + unchanged with an empty report. Benign inline formatting (````, ````) + and prose containing stray ``<``, ``>``, ``[`` are left untouched. + +Scope note (conceded, not hidden): this is a targeted defanger, not a full HTML +sanitizer. Text *between* escaped tags (e.g. a ``") + assert "