fix(lexicon,docs): the script-tag row reversed — commons is now the sole diverger

llm-security commit 90f576f (2026-08-10) dropped the closing-</script>-tag
requirement from hybrid-xss:script-tag, matching what the guard has always had.
Both now agree in shape (no closing tag required), still not byte-identical
([^><] vs [^>]). Commons' own extracted pattern is unchanged under
behaviour-preservation, so it is the only one of the three still requiring a
closing tag — the opposite of what the divergence doc said when it was written.

injection-lexicon.json gains source_fidelity.post_extraction_drift: the
existing 83/83-at-b0de0ca claim is untouched (extraction-commit fidelity never
decays), and the new 82/83-at-b1ba1fb count carries its own commit and date so
it doesn't read as a bare, decaying "vs HEAD" claim.

Verified directly against both upstream trees this session, not relayed:
llm-security's public remote re-fetched to b1ba1fb, guard read at HEAD 0dce50f.
This commit is contained in:
Kjell Tore Guttormsen 2026-08-11 12:45:44 +02:00
commit 6f79a6ea57
3 changed files with 81 additions and 3 deletions

View file

@ -11,6 +11,22 @@ case ids, disposition semantics). Each JSON file additionally carries its own
## [Unreleased] ## [Unreleased]
### Added
- `lexicon/injection-lexicon.json` (`0.5.1``0.6.0`) — `source_fidelity.post_extraction_drift`,
recording that `llm-security` commit `90f576f` (2026-08-10) dropped the closing-tag
requirement from `hybrid-xss:script-tag`'s source pattern, which this file — a frozen
extraction under behaviour-preservation — does not follow. `patterns_byte_identical_to_source`
(83, at extraction commit `b0de0ca`) is unchanged and still true; the new field adds the count
against `llm-security`'s later HEAD (82/83, measured against `b1ba1fb`, 2026-08-11) so a
consumer diffing against current upstream has a coordinate to resolve the mismatch against
instead of finding it unexplained. No pattern text, id, or existing key changed — additive
only, hence minor. Companion informative update in `docs/lexicon-port-divergence.md`, whose
`hybrid-xss:script-tag` row is reversed: commons is now the sole diverger of the three runtimes
on this one construct, not the guard.
Repository tag unaffected — no file set, key name, case id, or disposition semantics moved.
## [0.2.0] — 2026-08-11 ## [0.2.0] — 2026-08-11
The contract gained a normative MUST, which is why this is a release rather than a The contract gained a normative MUST, which is why this is a release rather than a

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))` | | `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: 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)` | **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)">` | | `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 The last row is the one of the 13 that does not reach report level. The guard's
@ -109,6 +109,48 @@ 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 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. 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 still carries the pre-`90f576f` form, unchanged, per this repository's
behaviour-preservation invariant — commons does not follow either upstream regex change
without an operator decision to re-extract. **Commons is now the sole diverger of the three**
on the closing-tag requirement: both `llm-security` and the guard match `<script src=x.js>`
and unclosed `<script>alert(1)`; commons' extracted pattern still requires `</script>`.
## Why they diverge: two different ReDoS mitigations of one table ## 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. This is not drift, and framing it as a bug in either repository would be wrong.

View file

@ -1,5 +1,5 @@
{ {
"version": "0.5.1", "version": "0.6.0",
"id": "injection-lexicon", "id": "injection-lexicon",
"description": "Prompt-injection pattern lexicon: the four pattern families a detector matches against untrusted text, and the severity family each belongs to. Data only. The variant building that feeds these patterns (normalisation, homoglyph folding, rot13, unicode-tag escalation) and the buried-payload heuristic are engine behaviour and are deliberately NOT described here.", "description": "Prompt-injection pattern lexicon: the four pattern families a detector matches against untrusted text, and the severity family each belongs to. Data only. The variant building that feeds these patterns (normalisation, homoglyph folding, rot13, unicode-tag escalation) and the buried-payload heuristic are engine behaviour and are deliberately NOT described here.",
"owasp": "LLM01", "owasp": "LLM01",
@ -112,7 +112,27 @@
"why_retracted": "The same transcription inversion in the opposite direction. The module writes pr[e\\u00e9]c[e\\u00e9]dentes? with escapes; this was the only one of the 83 patterns not byte-identical to source. Corrected in 0.4.0 to the module's spelling, which makes the count 83/83.", "why_retracted": "The same transcription inversion in the opposite direction. The module writes pr[e\\u00e9]c[e\\u00e9]dentes? with escapes; this was the only one of the 83 patterns not byte-identical to source. Corrected in 0.4.0 to the module's spelling, which makes the count 83/83.",
"behaviour_impact": "None. The two spellings are the same regular expression. Verified by compiling both forms and comparing match presence and offsets in Node (bare and `u` mode) and in Python `re` over accented, unaccented, uppercase and non-matching French inputs - identical results throughout." "behaviour_impact": "None. The two spellings are the same regular expression. Verified by compiling both forms and comparing match presence and offsets in Node (bare and `u` mode) and in Python `re` over accented, unaccented, uppercase and non-matching French inputs - identical results throughout."
} }
] ],
"post_extraction_drift": {
"$comment": "patterns_byte_identical_to_source above states fidelity AT the commit this file was extracted from (b0de0ca) and does not update when llm-security's source moves further - that is what behaviour-preservation means. Recorded here because llm-security has since moved on one pattern, and a consumer diffing this file against llm-security's current source would otherwise find an unexplained mismatch with no coordinate to resolve it against. See docs/lexicon-port-divergence.md for the full account.",
"extracted_from_commit": "b0de0ca",
"extracted_from_commit_date": "2026-08-09",
"count_at_extraction": 83,
"drift": [
{
"pattern_id": "hybrid-xss:script-tag",
"source_commit": "90f576f",
"source_commit_date": "2026-08-10",
"change": "llm-security dropped the closing-tag requirement: <script\\b[^>]*>[\\s\\S]*?<\\/script> became <script\\b[^>]*>, closing a recall hole where `<script>alert(1)` (unclosed) and `<script src=x.js>` produced no finding.",
"commons_pattern_unchanged": true,
"reconfirmed_no_further_drift_at": "b1ba1fb",
"reconfirmed_date": "2026-08-11",
"reconfirmed_method": "git diff 90f576f..b1ba1fb -- scanners/lib/injection-patterns.mjs on a fresh fetch of the public remote is empty for this pattern; no later commit touched it a second time."
}
],
"count_against_llm_security_head_as_of_2026_08_11": 82,
"count_against_llm_security_head_note": "b1ba1fb was llm-security's public-remote HEAD when this was measured, 2026-08-11. Not re-checked automatically; re-verify before citing against a later HEAD."
}
}, },
"families": [ "families": [
{ {