docs: retract the "neither runtime misses an attack" claim, fix three counts

Four corrections across the two informative documents, all of them to
claims this repository made and none of them to a data file.

The retraction is the important one. lexicon-port-divergence.md said
neither runtime misses an attack: every witness payload still produced a
finding, so the divergence was only in which labels were raised. That was
wrong in method rather than in arithmetic. The payloads had been run
against the UNION of every pattern table this repository holds - 111 rules
across the lexicon, active-content.json and secret-egress.json - and the
hit that rescued them came from active-content.json, which is the Python
guard's table. llm-security has no active-content table at all. Unioning
two runtimes' tables and reading the result as a statement about each of
them turns combined reach into individual coverage.

Re-measured through llm-security's own entry point scanForInjection() at
b0de0ca - the whole engine, normalisation, homoglyph folding, rot13 and all
four arrays - all three witness payloads return found: false. Not a
different label set: no finding at all. Controls in the same run behave
normally, so the {0,256} window is a real evasion window and the <script>
pattern really does require a closing tag. llm-security reached the same
three results independently and has logged them as a v8.x task.

Second: one of the 13 divergences does not reach report level. On the
iframe witness the guard misses hybrid-xss:iframe-src but its
hybrid-xss:javascript-uri fires at the same severity and the same OWASP
anchor, so a reader of its report still sees the payload flagged. Both
sides were re-measured to confirm. The report-level number is 12; the 13
stands as the number that blocks conformance/, because a fixture names
labels.

Third: extraction-plan.md said six of the confusables have no entry in the
fold map. It is zero - all 13 are keys in the 28-entry map, and
carriers.json has said so correctly since it was written. The prose, not
the data, carried the error, and the number six most likely came from
running the comparison the other way (five fold-map Cyrillic keys are not
confusables). It also called the diverging sets deliberately distinct;
llm-security states the U+0456 / U+0445 divergence is drift, not design,
and has logged it for v8.x. Those two readings imply opposite things about
whether the sets should be expected to converge.

Fourth: the seed-sources section still asserted that sessions here have no
read access to llm-security and that content arrives only as a dump. The
operator changed that on 2026-08-09. It now records the read-only pinned-
commit boundary, the still-absolute prohibition on the sibling working copy
and on writing, and the reason the change matters - four attribution
defects caught so far, every one a transcription artefact.

Status blocks updated: all eight JSON artefacts now exist, seven verified
differentially and calibration.json still honestly verified: false; the
conformance blocker is half-cleared, with naming ratified and the 13
divergent patterns still unresolved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SNMcqrfNyoLRQ7qXUFZnb9
This commit is contained in:
Kjell Tore Guttormsen 2026-08-09 22:54:41 +02:00
commit ff18768b71
3 changed files with 192 additions and 43 deletions

View file

@ -76,6 +76,25 @@ checks that were not run instead of attaching a caveat to a pass.
changed** — behaviour preservation holds and the finding is reported to the owning changed** — behaviour preservation holds and the finding is reported to the owning
repositories. repositories.
**Revised 2026-08-09 with one retraction.** The document claimed that *neither runtime
misses an attack*, on the grounds that every witness payload still produced a finding. It
does miss. That measurement ran the payloads against the **union of every pattern table
this repository holds**, and the rescuing hit came from `active-content.json` — the Python
guard's table. `llm-security` has no active-content table at all, so a union of commons
tables was read as a statement about each runtime separately. Re-measured through
`llm-security`'s own `scanForInjection()` at `b0de0ca`, all three witness payloads return
**`found: false`** — no finding whatsoever — while controls in the same run behave
normally. Three confirmed recall holes, which `llm-security` attributes to its v7.8.3 #24
ReDoS hardening and has logged as a v8.x task.
Also corrected: one of the 13 divergences does not reach report level, because the guard's
`hybrid-xss:javascript-uri` fires on the same witness at the same severity and anchor. The
report-level number is **12**; the 13 stands as the number blocking `conformance/`, since a
fixture names labels. And the `hybrid` severity question that the document reported rather
than resolved is now closed — the reported hint was right, the citation behind it was not.
Corrections are marked in place rather than edited away.
### Changed ### Changed
- `schema/finding.schema.json` **0.1.0 → 0.2.0** — the schema is **closed**. It was seeded - `schema/finding.schema.json` **0.1.0 → 0.2.0** — the schema is **closed**. It was seeded

View file

@ -40,10 +40,26 @@ Every JSON file carries a top-level `"version"` field. Every spec carries a
## v0.1.0 seed sources ## v0.1.0 seed sources
`llm-security` is the canonical and richest source. **This repository's sessions have no `llm-security` is the canonical and richest source. Security-critical tables (homoglyph map,
read access to it** — content arrives only as an operator-supplied dump. Security-critical secret patterns, malware signatures) MUST come from real source data, never from recollection
tables (homoglyph map, secret patterns, malware signatures) MUST come from real source or inference.
data, never from recollection or inference.
**The read boundary changed on 2026-08-09 (operator decision), and it matters to how the
rows below should be read.** This section previously stated that sessions here have no read
access and that content arrives only as an operator-supplied dump. Sessions may now **read
`llm-security` read-only from its public remote at a pinned commit** —
`ssh://git@git.fromaitochitta.com/open/llm-security.git` @ `b0de0ca`, verified as
`origin/main` — by cloning into a scratchpad. The sibling working copy on this machine
remains **absolutely off limits**, as does any form of writing.
The consequence is a method change, not just a convenience: files seeded from a dump are
**differentially verified against the module** rather than trusted as transcriptions. That
change has already caught four attribution defects — a re-escaping story that never happened
in the lexicon, an inverted escape in one pattern, a severity citation pointing at a file with
no severities in it, and two family names misremembered in a working note. Every one was a
transcription artefact. Where a row below still says a dump was the delivery, the
`provenance` block in the file itself records whether that dump has since been superseded by
a direct read.
| Target | Seed source in `llm-security` (unless noted) | | Target | Seed source in `llm-security` (unless noted) |
|---|---| |---|---|
@ -159,10 +175,28 @@ Recorded and deliberately **not** reconciled, in `cross_table_notes`: the reposi
holds three overlapping Cyrillic sets and two overlapping zero-width sets, and none agree holds three overlapping Cyrillic sets and two overlapping zero-width sets, and none agree
exactly. The zero-width carrier table includes U+00AD while the lexicon's pattern class does exactly. The zero-width carrier table includes U+00AD while the lexicon's pattern class does
not; the lexicon's class contains U+0456 which `CYRILLIC_CONFUSABLES` lacks, and not; the lexicon's class contains U+0456 which `CYRILLIC_CONFUSABLES` lacks, and
`CYRILLIC_CONFUSABLES` contains U+0445 which the class lacks; and six of the confusables `CYRILLIC_CONFUSABLES` contains U+0445 which the class lacks.
have no entry in the fold map. The dump states the presence set and the fold map are
deliberately distinct. The U+0456 / U+0445 divergence is reported to `llm-security` rather **Two claims that stood here until 2026-08-09 were wrong, and both are corrected rather
than fixed here. than quietly dropped.**
The first was a count: this paragraph said *six of the confusables have no entry in the fold
map*. It is **zero**. All 13 members of `CYRILLIC_CONFUSABLES` are keys in the 28-entry fold
map, and `codepoints/carriers.json` has said so correctly since it was written —
`cross_table_notes.cyrillic.in_confusables_but_not_foldable` is an empty array. The prose,
not the data, carried the error. The number six most likely came from the comparison run the
other way: the fold map holds **five** Cyrillic keys that are not confusables (U+0423,
U+0455, U+0456, U+0458, U+04CF), which `llm-security` confirms independently. A count
asserted in prose beside a data file that contradicts it is the cheapest kind of defect to
catch and the easiest to propagate, so the check is now: read the array, do not restate it.
The second was a characterisation. This paragraph said the sets are *deliberately distinct*.
`llm-security` states plainly that the U+0456 / U+0445 divergence **is drift, not design**.
They are not harmonising it now — that would be a behaviour change inside a
behaviour-preservation release — and have logged it as a v8.x task on their side. So the
divergence stays in the data exactly as it is, but it is recorded here as a known defect
awaiting a fix upstream, not as an intentional distinction. Those two readings imply opposite
things about whether a future consumer should expect the sets to converge.
### `signatures/secret-egress.json` — extracted 2026-08-09 ### `signatures/secret-egress.json` — extracted 2026-08-09
@ -322,15 +356,33 @@ pick a winner.
2. Every file in the layout above present and populated from verified seed data — or 2. Every file in the layout above present and populated from verified seed data — or
explicitly and visibly stubbed where the source was unavailable. explicitly and visibly stubbed where the source was unavailable.
**Status 2026-08-09: unmet, deliberately, and this is why no tag has been cut.** Six of **Status 2026-08-09, second revision: still unmet, and still why no tag has been cut —
the nine data files are present and verified. Three are neither present nor stubbed: but the remaining gap is two items, not three.** All **eight** JSON artefacts in the
`signatures/malware-signatures.json` (seed data not delivered), layout now exist. `signatures/malware-signatures.json` was the last, added once
`spec/decode-pipeline.md` (needs the decode implementation; a normative spec inferred `knowledge/signatures.json` became readable at `b0de0ca` and reproduced from the file
rather than from a dump.
Seven of the eight are differentially verified against their source; three of those
(`lexicon`, `mapping/owasp-map`, `signatures/malware-signatures`) are verified against the
source **module** at a pinned commit rather than against a transcription, and
`schema/finding.schema.json` is closed against its producer. The eighth,
`calibration/calibration.json`, still carries `verified: false` — its source arrived as a
prose summary with no code behind it, so no differential check was possible, and the file
names the checks that were not run rather than attaching a caveat to a pass.
Two items remain absent, and only one of them still blocks:
- `spec/decode-pipeline.md` — needs the decode implementation. A normative spec inferred
from a data dump would carry a `Status: normative` marker and pass every convention from a data dump would carry a `Status: normative` marker and pass every convention
check while asserting something nobody verified) and `conformance/` (blocked — see check in this document while asserting something nobody verified.
*Conformance sources* above: 105 of the guard's 134 cases are convertible, but a fixture - `conformance/`**half-unblocked.** The naming blocker is gone: the pattern id space
cannot name a finding until commons owns a pattern id both runtimes map to, and 13 is ratified by both runtimes as of 2026-08-09, so a fixture can name a finding. What
patterns behave differently between the two ports). remains is expected behaviour for the 13 patterns whose two ports genuinely differ.
`llm-security` has decided not to change strategy inside a behaviour-preservation
release, so those 13 stay divergent for now and are registered as known divergence per
pattern rather than resolved. Of the guard's 134 coverage cases, 105 are convertible to
static `input.txt` / `expected.json`; the other 29 assert a runtime's API surface, which
this repository does not own.
They are absent rather than stubbed on purpose: an empty `conformance/` and a They are absent rather than stubbed on purpose: an empty `conformance/` and a
contentless normative spec would both *pass* the mechanical checks in this document contentless normative spec would both *pass* the mechanical checks in this document

View file

@ -9,6 +9,22 @@ Produced 2026-08-09. Every number below came from a command; the scripts live in
scratchpad rather than in this repository, because executable code here would breach the scratchpad rather than in this repository, because executable code here would breach the
charter. They are reproducible from the method column. charter. They are reproducible from the method column.
**Revised the same day, after `llm-security`'s source became readable and both runtimes
replied.** Four things changed, and three of them are corrections to this file rather than
new results:
1. The claim that **neither runtime misses an attack** is **retracted**. It does. See
*What this does not show* — the measurement behind that claim unioned pattern tables
belonging to two different runtimes and read the result as a statement about each.
2. One of the 13 divergences does not reach report level, so **12** is the number that
changes what a report says. The 13 still blocks `conformance/`.
3. The `hybrid` **severity is resolved** to `high` — the reported hint was correct, and the
citation behind it was not.
4. The **pattern id space is ratified** by both runtimes.
Corrections are marked in place rather than edited away, because a reader who saw the first
version needs to know which sentence moved.
## What was compared ## What was compared
| Side | Artefact | Version | | Side | Artefact | Version |
@ -35,12 +51,24 @@ disagree about what the same source says.
| Differing regex text | string compare | 19 | | Differing regex text | string compare | 19 |
| — of those, provably equivalent | unescape commons' JS-isms (`\/``/`, `\uXXXX` → literal) and compare for string identity | **6/6 identical** | | — of those, provably equivalent | unescape commons' JS-isms (`\/``/`, `\uXXXX` → literal) and compare for string identity | **6/6 identical** |
| — of those, behaviourally divergent | differential match-set comparison (offsets + matched text), targeted corpus per pattern family | **13**, each with a concrete witness input | | — of those, behaviourally divergent | differential match-set comparison (offsets + matched text), targeted corpus per pattern family | **13**, each with a concrete witness input |
| — of those, divergent at REPORT level | re-check whether a sibling pattern raises an equivalent finding on the same witness | **12** — one of the 13 is a label-set difference only |
| Total input comparisons | count | 401 | | Total input comparisons | count | 401 |
| Flags | compare declared flags | **0 differences** | | Flags | compare declared flags | **0 differences** |
| Severity / family | commons family vs guard `severity` | **8 differences** (all `hybrid`) | | Severity / family | commons family vs guard `severity` | **8 differences** (all `hybrid`) — resolved, see *Severity* below |
64 identical + 6 escaping-only + 13 divergent = 83. 64 identical + 6 escaping-only + 13 divergent = 83.
**Read the 13 and the 12 as answering different questions.** Thirteen patterns produce
different label sets. Twelve of those change what a report would say. The gap is the
`hybrid-xss: iframe with executable src` row: the guard's version of that one pattern misses
the witness, but its `hybrid-xss: javascript: URI scheme` pattern fires on the same input at
the same severity and the same OWASP anchor, so a reader of the guard's report still sees a
`high` / `LLM01` finding on that payload. Measured, not reasoned: the guard's table matched
`hybrid-xss:javascript-uri` (high, LLM01) and nothing else; the Node engine matched both
`hybrid-xss: javascript: URI scheme` and `hybrid-xss: iframe with executable src`. A
`conformance/expected.json` still has to encode the difference, because a fixture names
labels — which is why the 13 stands as the number that blocks the corpus.
The 6 escaping-only pairs are equivalent by **string identity after unescaping**, not merely The 6 escaping-only pairs are equivalent by **string identity after unescaping**, not merely
by absence of a counter-example: `<\/system>` vs `</system>`, `text\/html` vs `text/html`, by absence of a counter-example: `<\/system>` vs `</system>`, `text\/html` vs `text/html`,
`(?:https?:\/\/…)` vs `(?:https?://…)`, `(?:~\/)?` vs `(?:~/)?`, and two `\uXXXX`-vs-literal `(?:https?:\/\/…)` vs `(?:https?://…)`, `(?:~\/)?` vs `(?:~/)?`, and two `\uXXXX`-vs-literal
@ -65,7 +93,12 @@ Divergence is **not one-directional**. Each side has inputs the other's pattern
| `sub-agent: spawn instruction with dangerous capability keywords` | **guard misses** | `create an agent that` + 20 filler words + `execute` | | `sub-agent: spawn instruction with dangerous capability keywords` | **guard misses** | `create an agent that` + 20 filler words + `execute` |
| `sub-agent: delegation with safety bypass instruction` | **guard misses** | `delegate to a new agent` + 200 chars + `bypass` | | `sub-agent: delegation with safety bypass instruction` | **guard misses** | `delegate to a new agent` + 200 chars + `bypass` |
| `hybrid-xss: <script> tag in content (agent context XSS)` | **commons misses**; span differs | `<script>alert(1)` (unclosed), `<script src=x.js>` | | `hybrid-xss: <script> tag in content (agent context XSS)` | **commons misses**; span differs | `<script>alert(1)` (unclosed), `<script src=x.js>` |
| `hybrid-xss: iframe with executable src (agent context XSS)` | **guard misses** | `<iframe data-x="<" src="javascript:alert(1)">` | | `hybrid-xss: iframe with executable src (agent context XSS)` | **guard misses the label, not the attack** | `<iframe data-x="<" src="javascript:alert(1)">` |
The last row is the one of the 13 that does not reach report level. The guard's
`hybrid-xss:javascript-uri` (`javascript\s*:`, high, LLM01) matches that witness, so the
payload is still flagged at the same severity and anchor; only the label set differs — one
finding instead of two. The remaining 12 rows change what a report says.
"Span only" means both sides produce a match on the same input but over different extents — "Span only" means both sides produce a match on the same input but over different extents —
the guard's match starts at an interior `<`. Whether that matters depends on whether a the guard's match starts at an interior `<`. Whether that matters depends on whether a
@ -120,30 +153,65 @@ repositories have to take, and it is not commons' to take for them.
## Severity: the 8 hybrid patterns ## Severity: the 8 hybrid patterns
Commons records the `hybrid` family with `severity: null` and a note that the seed dump did **Resolved 2026-08-09. The two sides never disagreed; only the evidence did.**
not supply it, so a consumer **MUST NOT** assume one. The guard's port assigns all eight
`high`.
**This has deliberately not been copied into commons.** The guard's port is a second-hand Commons recorded the `hybrid` family with `severity: null` and a note that the seed dump did
transcription, not the producing module; adopting its value would convert a documented gap not supply it, so a consumer **MUST NOT** assume one. The guard's port assigned all eight
into an unverified claim, which is the defect class this repository's changelog already `high`. Copying the guard's value would have converted a documented gap into an unverified
records twice. It is reported instead, as a strong hint that the Node source assigns `high`, claim, so it was reported instead — and the report was right: the value **is** `high`,
for `llm-security` to confirm from the module. confirmed at the module, and `lexicon/injection-lexicon.json` 0.5.0 now carries it. The eight
differences in the table above are closed.
The part worth keeping is where the value lives. It is not a field. The engine assigns it by
pushing `HYBRID_PATTERNS` matches straight into the `high` bucket at
`injection-patterns.mjs:274-281`. The guard's port cites `severity.mjs` — a file that
contains **no injection-family severity at all**. So the guard held the right value behind a
citation that leads nowhere, and a reviewer following that citation to check the number would
have found nothing and drawn no conclusion. Refusing to copy it was the right call for a
reason better than the one given at the time: not merely that a port is second-hand, but that
this particular port could not have read what it claimed to.
## What this does not show ## What this does not show
- **Not that either runtime misses an attack.** All witness payloads were re-tested against - ~~**Not that either runtime misses an attack.**~~ **Retracted 2026-08-09. It does.** This
every pattern table commons holds (111 compiled rules: 83 lexicon, 28 from bullet claimed that every witness payload still produced a finding via
`active-content.json` and `secret-egress.json`). All of them still produce a finding, via `active-content: constructs.raw-html`, so no attack went unflagged. The measurement behind
`active-content: constructs.raw-html`. The divergence is in the **finding set** — which it was wrong in method, not in arithmetic: the payloads were run against the **union of
labels are raised — not in whether anything is raised at all. That still matters, because a every pattern table this repository holds** — 111 rules across the lexicon,
finding set is exactly what a `conformance/expected.json` would encode. `active-content.json` and `secret-egress.json` — and the rescuing hit came from
- **Not that `llm-security` behaves as described here.** Only its extracted pattern table was `active-content.json`. That table is the **Python guard's**. `llm-security` has no
available. Whether the Node engine runs an active-content table that compensates cannot be active-content table at all. A union of commons tables is not any single runtime's
determined from this repository. coverage, and treating it as one turned two runtimes' combined reach into a claim about
- **Not dump-to-module fidelity.** Every check above proves the two *ports* agree or disagree. each of them.
That either matches `injection-patterns.mjs` remains `llm-security`'s assertion,
reproducible only in a session with read access to it. Measured properly, through `llm-security`'s own entry point `scanForInjection()` — the
whole engine, with normalisation, homoglyph folding, the rot13 variant and all four pattern
arrays, at `b0de0ca`:
| Witness | `scanForInjection()` result |
| --- | --- |
| `<script>alert(1)` (unclosed) | `found: false` — no finding at all |
| `<script src=x.js>` | `found: false` — no finding at all |
| `<a aria-label="` + 300 × `a` + `ignore">` | `found: false` — no finding at all |
Controls in the same run behave as expected: `<script>alert(1)</script>` returns `high`
(hybrid-xss), and the short aria-label variant returns `critical`. So the `{0,256}` window
is a real evasion window and the `<script>` pattern really does require a closing tag.
`llm-security` reached the same three results independently and attributes the cause to
their own v7.8.3 #24 ReDoS hardening, which traded recall for boundedness without seeing
the window. Three confirmed recall holes, logged there as a v8.x task.
What survives from the original bullet is only this: the divergence is *also* in the
finding set, which is what a `conformance/expected.json` encodes.
- **Not that the guard misses an attack.** The guard's side of the 13 was re-checked the same
way, and the one row that looked like a miss (`hybrid-xss: iframe with executable src`) is
covered by a sibling pattern at the same severity and anchor. Its remaining divergences are
label-set and span differences.
- **Not dump-to-module fidelity.** *Superseded 2026-08-09.* Every check above proves the two
*ports* agree or disagree. Commons' side is now settled separately: the lexicon is verified
byte-identical to `injection-patterns.mjs` at `b0de0ca`, 83/83, which is recorded in the
data file rather than here. The guard's fidelity to the module remains its own to
establish.
- **Not exhaustive.** The corpus is targeted per pattern family, 401 comparisons. Absence of a - **Not exhaustive.** The corpus is targeted per pattern family, 401 comparisons. Absence of a
witness proves nothing except for the 6 escaping-only pairs, which are settled by string witness proves nothing except for the 6 escaping-only pairs, which are settled by string
identity rather than by the corpus. identity rather than by the corpus.
@ -168,7 +236,17 @@ here — that port already carried both names, so the mapping came from source d
detection data is provably unmoved: stripping `id`, `aliases` and the new `pattern_id_space` detection data is provably unmoved: stripping `id`, `aliases` and the new `pattern_id_space`
block reproduces the previous committed file **byte for byte** (23 566 bytes, identical). block reproduces the previous committed file **byte for byte** (23 566 bytes, identical).
Neither runtime has agreed to it yet — both were asked by coord the same day — so the file **Ratified by both runtimes, 2026-08-09.** `llm-security` accepted the id space as-is,
records it as this repository's proposal rather than a ratified contract. The second half of including the 0.2.0 proposal, and treats an id change as breaking on the same terms; the
the blocker stands: the 13 divergent patterns still have no agreed expected behaviour, so guard confirmed the space its own port supplied. `lexicon/injection-lexicon.json` 0.5.0
their fixtures cannot be authored even now that they can be named. records both. The id is a cross-runtime contract now, not a proposal.
**The second half of the blocker stands, and it did not get smaller.** The 13 divergent
patterns still have no agreed expected behaviour, so their fixtures cannot be authored even
though they can now be named. `llm-security` has decided **not** to adopt the guard's regex
strategy at this point: v0.1.0 is a behaviour-preservation release on their side too, and
swapping strategies mid-vendoring would void their own golden gate. Both behaviours are
therefore registered as known divergence per pattern. Their three confirmed recall holes are
logged as a v8.x task; when it lands they will say so, and those rows can close then. Until
then the correct description of each is *"known divergence, `llm-security` side has an open
recall hole, measured 2026-08-09"* — not *"undecided"*.