llm-ingestion-pipeline-secu.../README.md
Kjell Tore Guttormsen 5397ba15a1 fix(security): harden 5 adversarial-review findings (M1/M2/M3 + m4/m6) via TDD
Pre-release hardening from an independent adversarial review; each fixed
test-first (failing test -> fix -> green). 214 tests pass.

- entropy (M1): decode-and-rescan now runs BEFORE false-positive suppression,
  so an SRI/media-prefixed injection blob is still decoded and lexicon-rescanned.
  Suppression gates only the entropy finding, never the decode.
- output/disposition (M3): the invisible-carrier invariant now holds on the
  persist gate. scan_output flags zero-width/BIDI presence and disposition
  treats those + lexicon:unicode-tags-present as any-tier carriers, so a carrier
  in model output fails secure even under a trusted policy.
- contract (M2): assert_credential_allowlist catches a bare <PROVIDER>_KEY
  (e.g. STRIPE_KEY) that the old regex silently missed (fail-open). Deliberately
  broad: also flags PARTITION_KEY/SORT_KEY as loud, allowlistable FPs -- fail-loud
  beats fail-silent for an isolation control.
- disposition (m6): guard runs decide inside its guarded block -> total
  fail-closed even on a malformed report.
- output (m4): egress placeholder suppression anchors word markers (example,
  todo, ...) to a word boundary, closing a fail-open where a real secret merely
  containing such a word was suppressed.

Docs: CHANGELOG Security subsection; README honest-limit for lexicon dedup (m5,
documented tradeoff, not fixed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyRCQMocjZ6SmSQ6JidJ2k
2026-07-05 10:45:05 +02:00

7.1 KiB

llm-ingestion-guard

Version Status Python Tests License

A reusable, minimal, dependency-light defensive layer for LLM ingestion pipelines — the write-time siblings of query-time chatbot guardrails.

Where mature guardrails (LLM Guard, NeMo Guardrails, Rebuff, Vigil, …) sit between a user and a model at query time, this library hardens the other shape: untrusted content flowing through an LLM enrichment/summarization/extraction step into a persisted, downstream-consumed artifact (RAG corpus, knowledge base, wiki). It packages the architectural contract — sanitize → fence → tool-less quarantined transform → per-stage capability isolation → scan output before commit → fail-secure — as composable, stdlib-first, framework-agnostic code.

The gap it fills is not "no one detects injection." It is a small library (not a hosted service, not a fine-tuned model) that packages the write-time ingestion contract — the part query-time tooling structurally cannot see, because a poisoned artifact committed at write time is read by a downstream agent whose guardrail never sees where it came from.

Status: v0.1, alpha. The stdlib-only core is built and tested — ten detector/contract modules and the top-level wiring, exercised by an end-to-end showcase and adversarial + false-positive corpora. The public API may still change. There are real limitations, stated plainly below; read them.

Install

pip install llm-ingestion-guard          # stdlib-only core, zero dependencies

Optional ML/judge detectors live behind extras ([ml], [judge]) and are not required — the core is deterministic and dependency-free.

Quickstart — the two bookends

The library never makes the model call itself. It gives you the two library-side halves around your own tool-less transform:

from llm_ingestion_guard import (
    prepare_input, screen_output, Disposition, PRESET_USER_UPLOAD,
)

prepared = prepare_input(untrusted_content)              # §6 1-2: sanitize + fence
enriched = your_model(prepared.fenced)                   # §6 3: tool-less — YOUR call
decision = screen_output(enriched, PRESET_USER_UPLOAD)   # §6 6-7: scan + dispose

if decision.disposition is Disposition.FAIL_SECURE:
    alert(gate_code=decision.reasons)                    # §6 8: minimal payload, no content
    raise SystemExit                                     # §6 7: halt — never persist

screen_output fails closed: if the scanner itself errors on crafted input, the disposition is FAIL_SECURE, never a silent persist. Pass transform_failed=True when your model call raised or fell back — a scan hit together with a transform failure is treated as a probable forced-fallback attack and halts regardless of trust tier.

Every primitive is also exported for pipelines that compose the checklist themselves — sanitize, scan_lexicon, scan_entropy, scan_output, neutralize, the decide / guard disposition machinery, and the contract asserters assert_tool_less / assert_credential_allowlist / scoped_env. See the end-to-end showcase for a full worked pipeline.

The reusable contract (adopt-this checklist)

The actual product is this checklist, encoded as code you wire in order:

  1. Sanitize before fence. Strip carrier classes (zero-width, BIDI, Unicode-tag, HTML comment, data:) from untrusted input first.
  2. Fence untrusted input. Spotlight-mark it in a randomized per-call delimiter; strip attacker fence markers from the payload.
  3. Tool-less transform. Call the model with zero tools. A successful injection then has nothing to act with.
  4. Per-stage capability isolation. The enrichment stage holds only the model key; the publish stage holds only the publish credential; no stage holds both.
  5. Treat output as data. Parse to a frozen schema; reject on structural violation. The output never reaches a shell, git, or a filesystem path.
  6. Scan output before persist. Run the lexicon + entropy over the emitted text. Verbatim-carried payloads and model-emitted instructions are caught here.
  7. Fail-secure on compound signals. Injection hit + transform failure = halt
    • alert, never a silent verbatim commit.
  8. Minimal alert payloads. Alert with a gate code + run ID, never content.

Steps 1-2 are prepare_input; steps 6-7 are screen_output; steps 3-5 are yours; the contract asserters harden step 3-4.

Honest limitations (shipped as a control)

Conceding these plainly is itself a control — it prevents the false assurance that a green scan means safe content:

  • Structural unsolvability at the text layer. Pattern/lexicon detection is bypassable in isolation; character-injection and novel phrasings evade it. The contract (tool-less transform, capability isolation, fail-secure) is what carries the security — the lexicon is defense-in-depth, not a wall.
  • Semantic / factual poisoning is invisible to lexicon + entropy: a factually false claim in clean prose carries no suspicious token. The grounding module ships only a SourceGroundingCheck seam — the deterministic core does not judge semantics; a [judge] implementation must be plugged in.
  • Adversarial-ML evasion can survive normalization; tokenizer mismatch between scanner and model leaves gaps.
  • Latent / dormant memory poisoning is not judgeable at write time.
  • Insider in-place edits by a trusted author are out of the untrusted-content threat model.
  • Text-only. The core is text -> findings: it parses no files (no pypdf/python-docx/archive deps). Extract text first, then scan it with the high-untrust upload provenance. OCR-embedded instructions and multimodal stego in images/PDFs are out of scope beyond the sanitizer's character-layer stripping.
  • Lexicon findings are deduplicated by pattern idcount=1 and the first offset are reported, so the same class matched across several channels/variants collapses to one finding at its first location. This keeps reports readable, but a caller that counts occurrences or needs every offset of a repeated pattern sees only the first: a deliberate readability tradeoff, not full positional coverage.

Out-of-scope (documented boundary)

Embedding/vector-layer defenses (OWASP LLM08, downstream of persist); multimodal steganography; query-time / runtime guardrails; semantic factuality verification.

Design & threat model

The contract is extracted from a working reference implementation (the claude-code-llm-wiki Stage B enrichment pipeline). Threat-model anchors: OWASP LLM Top-10 2025 (LLM01/02/04/05/06 strongest, LLM08 boundary, LLM09/10), PoisonedRAG, guardrail-evasion (arXiv 2504.11168), EchoLeak (CVE-2025-32711).

License

MIT — see LICENSE.