Squashed 'scanners/commons/' changes from 532d70d..4641a7b

4641a7b release(0.3.0): a detection pattern changed value — that is new here
1482c0b feat(schema,spec): give the §1.1 MUST a shape, since v0.2.0 shipped it without one
25a2cf9 feat(conformance): the witness case, and the derivation rule that had no room for it
c1b2385 fix(lexicon): converge script-tag on its source — re-extraction, not revision
6f79a6e fix(lexicon,docs): the script-tag row reversed — commons is now the sole diverger

git-subtree-dir: scanners/commons
git-subtree-split: 4641a7b5184047460e3f10038b615a61e7a4ac21
This commit is contained in:
Kjell Tore Guttormsen 2026-08-11 14:03:23 +02:00
commit bbd03f9b52
9 changed files with 585 additions and 25 deletions

View file

@ -94,7 +94,7 @@ Divergence is **not one-directional**. Each side has inputs the other's pattern
| `markdown: injection payload in link anchor text` | **guard misses** | `[click ignore this](http://e.com/a(b))` |
| `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` |
| `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)` | **reversed 2026-08-11, see below** | `<script>alert(1)` (unclosed), `<script src=x.js>` |
| `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
@ -109,6 +109,76 @@ consumer reports offsets or evidence text; it does not change whether a finding
The commons-side misses were confirmed in a real JS engine (Node v25.8.2, `RegExp` built from
the committed JSON), not only in the Python harness used for the differential.
### `hybrid-xss:script-tag`: the row reversed, 2026-08-11
At the time this file was written, `<script>alert(1)` (unclosed) and `<script src=x.js>` were
commons misses only in the sense that commons was a byte-identical extraction of
`llm-security`, so "commons misses" meant "commons and `llm-security` both miss, the guard
does not." That symmetry broke on `llm-security`'s side.
`llm-security` commit `90f576f` (`fix(llm-security): v8.x-A - close <script> recall hole, add
whole-table ReDoS gate`) replaced the source pattern
```
<script\b[^>]*>[\s\S]*?<\/script>
```
with
```
<script\b[^>]*>
```
dropping the closing-tag requirement that produced the recall hole this file first measured.
Both witnesses now match on `llm-security`'s side. Verified directly against the module at
commit `c67bad3` (2026-08-10, first commit carrying the change) and re-confirmed unmoved at
`b1ba1fb` (2026-08-11, current public-remote HEAD as fetched this session) — `git diff
c67bad3..b1ba1fb -- scanners/lib/injection-patterns.mjs` is empty, so no later commit touched
the pattern a second time.
The guard's pattern was never the same construct: `<script\b[^><]*>`, negated-class excluding
both `>` and `<` (`src/llm_ingestion_guard/injection_lexicon.json:463`, unchanged since
`de09711`, re-confirmed unmoved through current guard HEAD `0dce50f`, 2026-08-11). The two
patterns now agree on **shape** — both drop the closing-tag requirement, both are a single
negated-class run immediately after `<script\b` — but they are still not byte-identical: the
guard's excluded-character class also excludes `<`, `llm-security`'s does not. That one-byte
difference is unresolved on both sides (see *Open questions* in the coordination thread); it
does not reopen the closing-tag question, which is settled.
**Commons converged in lexicon `0.7.0` (2026-08-11), and this row is closed on the closing-tag
axis.** Through `0.6.0` commons was the sole diverger of the three: both `llm-security` and the
guard matched `<script src=x.js>` and unclosed `<script>alert(1)`, and commons' extracted pattern
still required `</script>`. Commons now carries `<script\b[^>]*>` — byte-identical to
`llm-security`'s `RegExp.prototype.source` at `b1ba1fb`, verified by compiling both.
The decision was **re-extraction, not revision**, and the distinction is what made it available
at all under the behaviour-preservation invariant. That invariant forbids commons from correcting
seed data it believes is wrong; it does not forbid re-reading the source after the source itself
moved and its owner asked. The lexicon's declared provenance is `llm-security`'s injection table,
and being loadable verbatim by that table's owner is the one thing it exists to do — the standing
alternative was a per-pattern override in `llm-security`'s own loader, i.e. a published core that
its source repository could not load as published. Operator decision, 2026-08-11, on
`llm-security`'s blocking coord request. Explicitly **not** decided by the 2-of-3 majority: a
count of implementations is not a mandate over detection data, and the provenance argument would
have held with the guard on either side.
The direction matters for what it cannot break: the new form matches a strict superset of the old
one, so relative to `0.6.0` it can add matches and cannot add a false negative. The reverse change
would not have been adoptable on the same reasoning.
**What is still open is the one-byte span difference**, and convergence did not touch it. The
guard excludes `<` from its negated class and the other two do not, so:
| Witness | commons `0.7.0` / `llm-security` | guard |
| --- | --- | --- |
| `<script>steal()</script>` | match | match |
| `<script src=x.js>` | match | match |
| `<script>alert(1)` | match | match |
| `<script <x>` | **match** | **no match** |
Measured by compiling all three forms, not reasoned from the character classes. The last row is
the whole residual divergence and neither side has claimed it.
## Why they diverge: two different ReDoS mitigations of one table
This is not drift, and framing it as a bug in either repository would be wrong.