Squashed 'scanners/commons/' changes from 4641a7b..7ce0ba7

7ce0ba7 docs(carriers): the third verdict exists, and publishing an alias is what takes it away
302625e fix(conformance): the tag carrier has no output: label, and our blocker claimed it did
fe5e6b2 docs(conventions): the merge button is off for a reason, and the reason now lives in the repo
2d86151 fix(divergence): our own iframe number read 3x low, and the reported cause was not the cause
daa7ba4 release(0.4.0): two values moved by two mechanisms, and the difference is the release
2eee7e1 feat(lexicon): both unbounded rows narrow to [^><]*, and the mechanism is new here
d467324 feat(signatures): the staleness we disclosed is closed by reading the module, not the message
4187715 docs(divergence): our own form has a number now, and it is quadratic
0e765a0 docs(security): the attack surface here is data, so the report route had to say where a wrong entry gets fixed
d96fbbf docs(divergence): the span row had one witness; llm-security measured five more

git-subtree-dir: scanners/commons
git-subtree-split: 7ce0ba706cadd032ec3c16622dcfdb5ce4dc32d6
This commit is contained in:
Kjell Tore Guttormsen 2026-08-13 20:08:20 +02:00
commit bc23b07cf9
8 changed files with 844 additions and 41 deletions

View file

@ -1,5 +1,5 @@
{
"version": "0.7.0",
"version": "0.8.0",
"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.",
"owasp": "LLM01",
@ -144,9 +144,30 @@
],
"count_against_llm_security_head_as_of_2026_08_11": 83,
"count_against_llm_security_head_note": "b1ba1fb was llm-security's public-remote HEAD when this was measured, 2026-08-11, and after the 0.7.0 re-extraction the count against it is 83/83. Was 82/83 in versions 0.6.0 and earlier. Not re-checked automatically; re-verify before citing against a later HEAD.",
"count_against_llm_security_head_is_now_undefined": "This metric HAS NO TARGET from 0.8.0 onward, and reporting a number for it would be reporting a comparison that cannot be made. It compared commons' patterns against llm-security's local regex literals; at their commit `be14867` those literals were removed and the tables are built from this file instead. Measured on their published surface at `47905da`: `scanners/lib/injection-patterns.mjs` contains zero `pattern: /.../` literals. A comparison against their vendored COPY of this file is a subtree-freshness check, not an independent agreement measurement, and must not be cited as though it were the latter.",
"count_against_extraction_commit": 82,
"count_against_extraction_commit_note": "From 0.7.0 this file is 82/83 byte-identical to b0de0ca, the commit it was originally extracted from. That number will not improve: the re-extracted pattern is deliberately the newer form.",
"residual_divergence_vs_guard": "Closing the llm-security divergence does NOT make this pattern universal. The guard's port is <script\\b[^><]*> - its negated class also excludes `<` - so commons and the guard now agree on SHAPE (opening tag only) and still differ by one byte in the excluded set. Consequence, measured on construction rather than assumed: an input like `<script <x>` matches the commons/llm-security form and not the guard's. That row stays open in docs/lexicon-port-divergence.md as `span differs`, and neither side has claimed it."
"residual_divergence_vs_guard": "CLOSED IN 0.8.0, and the account through 0.7.0 is preserved here because a consumer diffing against v0.3.0 needs it. That account read: 'Closing the llm-security divergence does NOT make this pattern universal. The guard's port is <script\\b[^><]*> - its negated class also excludes `<` - so commons and the guard now agree on SHAPE (opening tag only) and still differ by one byte in the excluded set. Consequence, measured on construction rather than assumed: an input like `<script <x>` matches the commons/llm-security form and not the guard's.' That one-byte difference no longer exists: see `owner_directed_changes` below, which moved commons to `[^><]*` on both unbounded rows. The `span differs` row in docs/lexicon-port-divergence.md is retired by that change, not by anyone conceding the argument."
},
"owner_directed_changes": {
"$comment": "A THIRD mechanism, distinct from both `retracted` (this file described its own provenance wrongly) and `post_extraction_drift` (the source moved and this file re-read it). Here the source's OWNER decided a value and asked this file to carry it, because the dependency direction has inverted: as of llm-security `be14867`, their four injection tables are built from THIS file and hold zero local regex literals - measured here on their published surface at `47905da`, and their vendored copy of `lexicon/injection-lexicon.json` was confirmed byte-identical to commons 0.7.0. Re-extraction is therefore not available as a mechanism for these rows: there is no upstream literal left to re-read. Recording this as drift would have been false in the specific way that matters - it would say the source moved and commons followed, when the source now reads commons.",
"changes": [
{
"pattern_ids": [
"hybrid-xss:script-tag",
"hybrid-xss:iframe-src"
],
"version": "0.8.0",
"date": "2026-08-11",
"change": "The unbounded negated class between the tag name and the rest of the pattern was narrowed from `[^>]*` to `[^><]*` on both rows: `<script\\b[^>]*>` became `<script\\b[^><]*>`, and `<iframe\\b[^>]*src\\s*=\\s*[\"'][^\"']*(?:javascript:|data:text\\/html)` became the same with `[^><]*`.",
"why": "ReDoS. Both forms are quadratic in scan length on input that repeats the literal tag prefix without ever supplying a `>`: each occurrence is a match start and `[^>]*` scans to end of input from each one. Measured here in Node v25.8.2 on the repeated-prefix unit, x4 per doubling for both patterns: script-tag 32.65 / 113.36 / 479.02 / 1988.83 / 7772.25 ms and iframe-src 39.23 / 131.76 / 574.94 / 2469.55 / 9449.94 ms at 16k / 32k / 64k / 128k / 256k chars. Under `[^><]*` the same inputs cost 0.08-0.66 ms and 0.10-1.00 ms - flat, not merely faster.",
"recall_cost": "One input class stops matching: content carrying a literal `<` between the tag name and the `>`, such as `<script <x>`. Measured over ALL 90 conformance cases rather than only the four that cite these ids: 0 cases lost a match and 0 gained one, under both patterns. The dropped class is real but unwitnessed by the corpus.",
"why_not_bounded": "`[^>]{0,256}` was on the table and is linear, but a bound is paddable - an attacker who pads the attribute run past the bound falls out of the pattern, which is the attribute-padding evasion class llm-security already carries open as v8.x-A3. `[^>]{1,256}` is worse than a trade: it requires at least one character before the `>`, so it stops matching bare `<script>` and drops `hybrid-xss__script-tag` and `active__raw-html` from the corpus. That is llm-security's measurement, reproduced here as the reason the six bounded html-obfuscation rows are NOT the analogue to reach for. `[^><]*` is linear and has no bound to pad past, so it is better on both axes.",
"decided_by": "llm-security, the declared source of this table, in their coord message 2026-08-11T15:59:49Z, having re-measured rather than accepted commons' report; carried here on operator decision, 2026-08-11.",
"provenance_reasoning": "The justification is the SAME one that kept commons on `[^>]` through 0.7.0, applied after the facts moved. docs/lexicon-port-divergence.md recorded at 0.7.0 that commons carried `[^>]` because the declared provenance is llm-security's table and not because that regex won on merit, and stated explicitly that commons would still track its declared source. The declared source has now chosen `[^><]*`. Not merit, and NOT majority: the guard reached `[^><]` first and independently (their commit `cff0437`), so all three runtimes now agree - but a 3-of-3 count is not what decided this, and would not have been sufficient on its own.",
"consumer_impact": "This changes pattern TEXT, so it is not a no-op release of the v0.3.1 class. Any consumer asserting byte-identity of this file against commons v0.3.0 will go red by construction. Pattern ids, labels, aliases, family membership and the 83 count are all unchanged."
}
]
}
},
"families": [
@ -977,7 +998,7 @@
{
"id": "hybrid-xss:script-tag",
"label": "hybrid-xss: <script> tag in content (agent context XSS)",
"pattern": "<script\\b[^>]*>",
"pattern": "<script\\b[^><]*>",
"flags": "i",
"aliases": {
"llm_security": "hybrid-xss: <script> tag in content (agent context XSS)",
@ -1007,7 +1028,7 @@
{
"id": "hybrid-xss:iframe-src",
"label": "hybrid-xss: iframe with executable src (agent context XSS)",
"pattern": "<iframe\\b[^>]*src\\s*=\\s*[\"'][^\"']*(?:javascript:|data:text\\/html)",
"pattern": "<iframe\\b[^><]*src\\s*=\\s*[\"'][^\"']*(?:javascript:|data:text\\/html)",
"flags": "i",
"aliases": {
"llm_security": "hybrid-xss: iframe with executable src (agent context XSS)",