chore(release): 1.4.2
This commit is contained in:
parent
c72e648a94
commit
1241075ef8
7 changed files with 108 additions and 21 deletions
81
CHANGELOG.md
81
CHANGELOG.md
|
|
@ -5,6 +5,87 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.4.2] — 2026-10-29
|
||||
|
||||
### Fixed — invisible-character smuggling passed with 0 findings
|
||||
|
||||
Two published 2025 techniques reached the model through every gate untouched.
|
||||
**Variation-selector smuggling** (Butler, 2025-02; Gao et al., 2025-10) hangs one
|
||||
selector per hidden byte off a visible character; **Sneaky Bits** (Rehberger,
|
||||
2025-03) encodes bits in U+2062 and U+2064. On `1.4.1` both gave **0 findings**
|
||||
on the input path and on the output gate, and every hidden codepoint stayed in the
|
||||
"sanitized" text. Measured more broadly in session 1: 17 of 19 invisible
|
||||
character classes gave 0 findings.
|
||||
|
||||
The fix starts from a principled denominator instead of a hand-picked list:
|
||||
Default_Ignorable_Code_Point | format characters (Cf) | private use (Co) in
|
||||
Unicode 18.0, **141 674 codepoints**, frozen with the source files' sha256 in
|
||||
`docs/threat-2026/codepoints.json`. The guard knew 142 of them. Every one is now
|
||||
stripped by `sanitize` and flagged by the output gate unless a known legitimate
|
||||
use exempts it **on its context**. One predicate serves both surfaces, and a test
|
||||
checks all 141 674 codepoints, one by one, on both.
|
||||
|
||||
The exemptions are those the ZWJ fix established: judge by context, never by
|
||||
identity alone. A *run* is a carrier whatever it stands after.
|
||||
|
||||
- one VS15/VS16 after an emoji base (371/371 of Unicode 18.0's emoji variation
|
||||
sequences): ⚠️ ⏱️ ❤️ 1️⃣ survive byte-identical;
|
||||
- one ideographic selector after a CJK ideograph (registered name variants, and
|
||||
the 1 002 CJK compatibility sequences);
|
||||
- ZWNJ/ZWJ next to an Arabic-script or Indic letter, with neither neighbour
|
||||
invisible. Persian, Urdu, Hindi and Malayalam orthography stops being an
|
||||
any-tier carrier. That closes the ZWNJ false-positive class 0.6.1 parked for
|
||||
want of a corpus to verify a rule against. Measured on 9 Wikipedia articles
|
||||
(fa ×3, hi ×2, ml, ur, he, ar), articles with a carrier finding went from
|
||||
**6/9 to 2/9**, and the two left are real (a ZWSP where ZWNJ belongs; two ZWNJ
|
||||
in a row);
|
||||
- LRM/RLM/ALM in a document that carries right-to-left text;
|
||||
- an Arabic number sign (U+0600–0605 and kin) directly before a digit.
|
||||
|
||||
Private use is caught in full: an icon-font glyph is blocked rather than leaving
|
||||
a ~17-bit channel per visible gap. That decision, the number-sign rule and the
|
||||
one-selector-per-ideograph rule are the operator's (2026-10-28). What each
|
||||
exemption leaves open, and what is now blocked despite a legitimate use, is in
|
||||
LIMITATIONS.
|
||||
|
||||
**The lexicon's word-split row** (`unicode:zero-width-in-word`, `\w[ZW]\w`)
|
||||
matched every Persian word that carries its required ZWNJ. It now judges the
|
||||
joiner it matched by the same predicate. The regex itself is unchanged and stays
|
||||
byte-identical with the commons lexicon, and a Latin word split still fires.
|
||||
|
||||
### Added — labels
|
||||
|
||||
`sanitize:variation-selector`, `sanitize:private-use`, `sanitize:invisible-format`
|
||||
and their output-gate counterparts `output:variation-selector-present`,
|
||||
`output:private-use-present` and `output:invisible-format-present`, plus
|
||||
`output:unicode-tag-present`. That last one is the complete tag check on the
|
||||
output side, because the lexicon's narrower one misses U+E0000. All seven are
|
||||
any-tier carriers (`FAIL_SECURE` in every tier), as the zero-width and BIDI labels
|
||||
were. **A consumer filtering on the old labels sees new ones.** Labels are
|
||||
detection behaviour, not the frozen surface.
|
||||
|
||||
### Measured — the guard rule, before and after
|
||||
|
||||
- **Attack cases:** A01 and A02 are green on both arrivals. Row 3 of the
|
||||
threat-2026 plan stands at 10 caught, 1 documented gap and 8 red rows (8 of 31
|
||||
arrivals, down from 12).
|
||||
- **Test suite:** 1049 passed, 8 strict xfail (was 935 + 12).
|
||||
- **Coverage matrix:** 136/136 caught classes (was 130/130; six carrier cases
|
||||
added); 6/6 documented gaps hold.
|
||||
- **Pinned OKF corpus** (`_okf-upstream` @ `3fcbb9f`): unmoved, 6/53 parse and
|
||||
47 `fail_secure` / 31 `warn` on import, identical before and after.
|
||||
- **False-positive populations** (`docs/fp-sweep.py`, upload preset, documents
|
||||
disposed non-WARN): unmoved — vendor-harvest 106/198, generated-notes 196/2918, reference-corpus 108/389,
|
||||
identical line for line before and after. Positive control: doubling the
|
||||
VS16 that a reference-corpus document already carries moves it from
|
||||
`quarantine_review` to `fail_secure`.
|
||||
- **Runtime:** the classes are located with one regex pass and judged per hit.
|
||||
A 1 000 000-character document made entirely of carriers costs 1.4 s in
|
||||
`sanitize`, and a clean one 0.05 s. The RTL context is looked up once per
|
||||
document, so a document of directional marks stays linear (pinned). The ReDoS
|
||||
sweep collects 154 patterns (the two new character classes included) and
|
||||
flags neither.
|
||||
|
||||
## [1.4.1] — 2026-09-23
|
||||
|
||||
### Fixed — a flow sequence refused `#` and `:` where YAML reads them as text
|
||||
|
|
|
|||
12
CLAUDE.md
12
CLAUDE.md
|
|
@ -11,7 +11,7 @@ framework-agnostisk kode.
|
|||
Referanse-implementasjon: `claude-code-llm-wiki` Stage B (`tools/wiki_ingest/`).
|
||||
Lexikon-seed: `injection-patterns.mjs` fra `llm-security`-pluginen.
|
||||
|
||||
Repoet er på **v1.4.1** (release-commiten bumper alle levende versjonsflater:
|
||||
Repoet er på **v1.4.2** (release-commiten bumper alle levende versjonsflater:
|
||||
`pyproject.toml`, `__init__.py`, README-badge + status + install-pin,
|
||||
ADOPTION-BRIEF, BRIEF, CHANGELOG-overskrift). NB: v1.4.0-taggen bærer `1.3.0` i
|
||||
dokumentflatene — dens release-commit daterte bare CHANGELOG-en — og tags
|
||||
|
|
@ -61,8 +61,14 @@ som `active:raw-html-link` (MEDIUM), og en tagg hvis hele affordans ER en URL de
|
|||
ikke bærer (`</a>`, `<Frame>`, `<video />`) er inert. Klassifisering skjer i
|
||||
`active_tag_class`; `is_active_tag` er en tynn wrapper, og census patcher den
|
||||
FØRSTE (en boolsk patch kan ikke uttrykke en regradering).
|
||||
ZWJ (U+200D) dømmes på KONTEKST, ikke identitet — unntas kun mellom to emoji, på
|
||||
begge flater (`sanitize` eier predikatet, `output` importerer det).
|
||||
**Usynlige tegn (1.4.2):** hele nevneren i `docs/threat-2026/codepoints.json`
|
||||
(DICP ∪ Cf ∪ Co, Unicode 18.0, 141 674 tegn) fanges, eller unntas på KONTEKST:
|
||||
én VS15/16 etter emoji, én ideografisk velger etter CJK, ZWNJ/ZWJ ved arabisk
|
||||
skrift eller indisk skrift, LRM/RLM/ALM i dokument med RTL, arabisk talltegn foran
|
||||
siffer, ZWJ mellom to emoji. En rekke er alltid bærer, og privat bruk fanges helt
|
||||
(operatøren 28.10). Ett predikat (`sanitize._invisible_carriers`) på begge flater.
|
||||
Leksikonraden for orddeling dømmer leddet sitt med samme predikat, mens regex-en
|
||||
er byte-lik med commons.
|
||||
Start med `docs/BRIEF.md` for design, `README.md` for bruk, `docs/PLAN.md` for
|
||||
byggerekkefølgen.
|
||||
|
||||
|
|
|
|||
24
README.md
24
README.md
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
Write-time defensive layer for Python pipelines that persist LLM output: sanitize, fence, tool-less quarantined transform, capability isolation, scan before persist, fail-secure.
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
|
@ -33,7 +33,7 @@ at write time, never assumed from the format. Any pipeline ingesting external da
|
|||
into an agent-read store has this shape; an OKF wiki is its canonical form — which
|
||||
is why the guard ships a first-class OKF adapter (below).
|
||||
|
||||
**Status:** `v1.4.1`. The stdlib-only core — its detector, contract, and
|
||||
**Status:** `v1.4.2`. The stdlib-only core — its detector, contract, and
|
||||
OKF-adapter modules plus the top-level wiring — is built and tested, exercised by
|
||||
an end-to-end showcase and adversarial + false-positive corpora. The exported
|
||||
Python surface is now frozen under semver: nothing exported is removed, renamed or
|
||||
|
|
@ -58,14 +58,14 @@ are real limitations, stated plainly below; read them.
|
|||
Not on PyPI. The guard is distributed from its Forgejo origin — pin a release tag:
|
||||
|
||||
```bash
|
||||
pip install "llm-ingestion-guard @ git+https://git.fromaitochitta.com/open/llm-ingestion-pipeline-security.git@v1.4.1"
|
||||
pip install "llm-ingestion-guard @ git+https://git.fromaitochitta.com/open/llm-ingestion-pipeline-security.git@v1.4.2"
|
||||
```
|
||||
|
||||
Or take a version range from the Forgejo package index, which carries `1.4.1`
|
||||
(and `1.1.0`; `1.2.0`–`1.4.0` were never published there):
|
||||
Or take a version range from the Forgejo package index, which carries `1.4.2`,
|
||||
`1.4.1` and `1.1.0` (`1.2.0`–`1.4.0` were never published there):
|
||||
|
||||
```bash
|
||||
pip install --index-url https://git.fromaitochitta.com/api/packages/open/pypi/simple/ "llm-ingestion-guard>=1.4.1,<2"
|
||||
pip install --index-url https://git.fromaitochitta.com/api/packages/open/pypi/simple/ "llm-ingestion-guard>=1.4.2,<2"
|
||||
```
|
||||
|
||||
The `open/` mirror is anonymously readable, so CI needs no deploy key, token, or
|
||||
|
|
@ -204,16 +204,16 @@ tier + disposition, emitted to `log.md`).
|
|||
## What it protects against
|
||||
|
||||
Concrete attack classes, grouped by OWASP LLM Top-10 (2025) anchor. Every row is
|
||||
driven by a **live payload** in the coverage matrix — run it to watch all 134 pass
|
||||
driven by a **live payload** in the coverage matrix — run it to watch all 142 pass
|
||||
in your own environment:
|
||||
|
||||
```bash
|
||||
python -m llm_ingestion_guard.coverage # 130/130 classes; exit 0 = all as documented
|
||||
python -m llm_ingestion_guard.coverage # 136/136 classes; exit 0 = all as documented
|
||||
```
|
||||
|
||||
| Anchor | Attack classes it stops (representative) |
|
||||
|---|---|
|
||||
| **LLM01 · prompt injection** | 83 instruction-override lexicon classes (ignore / forget / disregard / suspend-constraints, role-play, jailbreak, …); hidden carriers — zero-width stego, BIDI override, Unicode-tag stego, HTML-comment, `data:` URI — stripped on input **and** re-checked at the persist gate; high-entropy base64/hex blobs; an injection hidden inside a base64 blob (decoded, then re-scanned) |
|
||||
| **LLM01 · prompt injection** | 83 instruction-override lexicon classes (ignore / forget / disregard / suspend-constraints, role-play, jailbreak, …); hidden carriers — every invisible codepoint in Unicode 18.0 (zero-width, BIDI override, Unicode-tag, variation-selector and Sneaky-Bits smuggling, private use; legitimate emoji, CJK, Arabic-script, Indic and right-to-left uses exempt on context), HTML-comment, `data:` URI — stripped on input **and** re-checked at the persist gate; high-entropy base64/hex blobs; an injection hidden inside a base64 blob (decoded, then re-scanned) |
|
||||
| **LLM02 · sensitive-info disclosure** | Secret egress in output — AWS keys, tokens, private keys, the credential set; a base64-*wrapped* secret (decoded → `decoded:egress:*`) |
|
||||
| **LLM05 · improper output handling** | Zero-click exfil carriers (EchoLeak, CVE-2025-32711) — markdown-image auto-fetch, inline / reference / autolink links, raw active HTML, standalone `data:` URIs; non-`https` `resource` URLs |
|
||||
| **LLM06 · excessive agency** | A tool surface on the quarantined transform; credential use beyond the per-stage allowlist; capability isolation (`scoped_env`) |
|
||||
|
|
@ -233,8 +233,8 @@ out in [Known limitations](#known-limitations).
|
|||
|
||||
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.
|
||||
1. **Sanitize before fence.** Strip carrier classes (every invisible codepoint
|
||||
not exempt on its context, 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
|
||||
|
|
@ -290,7 +290,7 @@ a green scan means safe content. The highest-impact items:
|
|||
two of the three corpora are living, so the cells are not rewritten in place.
|
||||
Method and before/after: [`docs/rawhtml-census.py`](docs/rawhtml-census.py).
|
||||
|
||||
**Full list — 45 items, each with the mechanism, plus the out-of-scope boundary:**
|
||||
**Full list — 46 items, each with the mechanism, plus the out-of-scope boundary:**
|
||||
[`docs/LIMITATIONS.md`](docs/LIMITATIONS.md). Several carry field measurements from
|
||||
consumer corpora, including the false positives the URL-shape rule actually produces.
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
especially one converging on Google's Open Knowledge Format (OKF v0.1) — and needs
|
||||
to decide **when** and **where** to add a write-time ingestion guard.
|
||||
|
||||
**Status of the guard:** `v1.4.1`. Stdlib-only core, framework-agnostic. The
|
||||
**Status of the guard:** `v1.4.2`. Stdlib-only core, framework-agnostic. The
|
||||
exported Python surface is frozen under semver — nothing exported is removed,
|
||||
renamed or given a different meaning without a `2.0.0`. Detection behaviour is
|
||||
*not* frozen: severities, thresholds and lexicon entries are calibration and move
|
||||
|
|
@ -142,9 +142,9 @@ live payload:
|
|||
python -m llm_ingestion_guard.coverage # exit 0 = all as documented
|
||||
```
|
||||
|
||||
As of `v1.4.1`: **130 / 130 defended classes demonstrated (recall 100%)** and **6 /
|
||||
As of `v1.4.2`: **136 / 136 defended classes demonstrated (recall 100%)** and **6 /
|
||||
6 documented gaps still hold** (a *closed* gap fails the test, forcing a doc
|
||||
update). The matrix is the single source of truth for the test suite (**910
|
||||
update). The matrix is the single source of truth for the test suite (**1049
|
||||
passing**), which also asserts total recall, that every lexicon pattern has a
|
||||
case (so the matrix cannot fall behind the lexicon), the full LLM02 secret-egress
|
||||
set, and the container-layer front-end (CSV formula-injection, zip-slip/bomb,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
**A reusable, minimal, dependency-light defensive layer for LLM *ingestion*
|
||||
pipelines — the write-time siblings of query-time chatbot guardrails.**
|
||||
|
||||
Status: implemented — v1.4.1, exported surface frozen under semver. This document defines what the repo contains
|
||||
Status: implemented — v1.4.2, exported surface frozen under semver. This document defines what the repo contains
|
||||
and why; the stdlib-only core is built and tested (see `README.md` for usage and
|
||||
`docs/PLAN.md` for the build order).
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||
|
||||
[project]
|
||||
name = "llm-ingestion-guard"
|
||||
version = "1.4.1"
|
||||
version = "1.4.2"
|
||||
description = "Write-time defensive layer for Python pipelines that persist LLM output: sanitize, fence, tool-less quarantined transform, capability isolation, scan before persist, fail-secure."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ from .grounding import (
|
|||
)
|
||||
from . import okf
|
||||
|
||||
__version__ = "1.4.1"
|
||||
__version__ = "1.4.2"
|
||||
|
||||
|
||||
# --- §6 bookends: the two library-side halves around the transform ---------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue