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:
parent
bbd03f9b52
commit
bc23b07cf9
8 changed files with 844 additions and 41 deletions
287
CHANGELOG.md
287
CHANGELOG.md
|
|
@ -9,9 +9,292 @@ Versioning note: the repository tag versions **the contract** (file set, key nam
|
|||
case ids, disposition semantics). Each JSON file additionally carries its own
|
||||
`"version"` field, bumped when that file changes.
|
||||
|
||||
## [Unreleased]
|
||||
## [0.4.3] — 2026-08-11
|
||||
|
||||
Nothing yet.
|
||||
**No fixture, id or `expected.json` moved — a runtime that passes `0.4.2` passes `0.4.3`
|
||||
unchanged. What changed is a claim this repository made about a runtime it does not own, and the
|
||||
claim was wrong on the day it was written.**
|
||||
|
||||
### Fixed
|
||||
|
||||
- `conformance/manifest.json` `0.3.2` → `0.3.4` — the `scope_planned.blockers` entry for
|
||||
`codepoints/carriers.json` described the guard as emitting two stage-coupled labels per carrier,
|
||||
"the same split for bidi and unicode-tag". The artifact-side label for tags is
|
||||
`lexicon:unicode-tags-present`, emitted from `lexicon.py`, and `output.py` carries a comment
|
||||
saying it deliberately does not repeat it there. **Wrong when written, not stale:** checked at
|
||||
`e671edb` — the commit the sibling secret-egress blocker was measured against — where
|
||||
`coverage.py` already asserted that label, and re-measured at `a59184b`.
|
||||
|
||||
The correction moves the blocker rather than removing it. The guard's `Finding` carries a
|
||||
`detector` field beside `label`, and the prefix is that field's value: `detector="lexicon"` on
|
||||
`lexicon:unicode-tags-present`, `"output"` on `output:zero-width-present`. **The prefix names
|
||||
the detector, not the pipeline stage** — and for tags a single detector serves both entry
|
||||
points, which is why there is no sixth `output:` label to find. "A commons id would have to be
|
||||
invented stage-neutral" was never the problem. Six labels exist to adopt verbatim, the way the
|
||||
83 lexicon ids were adopted from this same runtime's port.
|
||||
|
||||
What blocks adoption is measured and named instead, at llm-security `47905da`: `sanitize:`
|
||||
asserts a strip that runtime does not perform (`scanners/unicode-scanner.mjs` exports one entry
|
||||
point, `scan(targetPath, discovery)`, reporting presence with `scanner: 'UNI'`, a prose title
|
||||
and no id); three of the six name a persist gate it does not have, which the corpus already has
|
||||
a verdict for — §1.1 `not-applicable`, attaching to a declared **table** — but which
|
||||
**publishing the alias is what takes away**: that runtime's suite walks each vendored file for
|
||||
any node carrying `aliases.llm_security` and asserts every registered table is declared, so one
|
||||
aliased carrier id forces `codepoints/carriers.json` into a declared set of what is today the
|
||||
lexicon alone, obliging it to run all six cases and converting the three artifact-side ones into
|
||||
failures; and the entry point pinned for it in `measurement.runtimes` (`scanForInjection`) does not
|
||||
reach carriers at all, so carrier cases need a per-scope entry point this manifest expresses
|
||||
nowhere. Both runtimes already build their carrier sets from `codepoints/carriers.json`, so the
|
||||
divergence is in what a finding is *called* and where it can be *observed*, never in which code
|
||||
points are carriers.
|
||||
|
||||
### Asked, not decided
|
||||
|
||||
- The three objections went to both runtimes over coord on 2026-08-11 as a decision request, each
|
||||
asked the question only it can answer. **Nothing was minted.** A case id is contract surface
|
||||
consumers pin against, and publishing a single carrier alias is itself irreversible — it widens
|
||||
another runtime's declared table set by force of that runtime's own test suite. Minting first
|
||||
would have made a proposal into a fait accompli. The manifest records the request, so a later
|
||||
reader can tell "asked, unanswered" from "nobody asked".
|
||||
|
||||
- **A correction followed the request the same day, on our own error.** The request asserted that
|
||||
the corpus had no third verdict for a case a runtime cannot reach. It has one — §1.1
|
||||
`not-applicable` — and this repository wrote that section. The question was put before its own
|
||||
normative spec was re-read; the follow-up says so to both runtimes and restates the choice as
|
||||
mint-input-side-only, accept three standing failures, or publish a guard-only id space with no
|
||||
`llm_security` alias at all.
|
||||
|
||||
## [0.4.2] — 2026-08-11
|
||||
|
||||
**No data file changed and no pattern moved.** A runtime that passes `0.4.1` passes `0.4.2`
|
||||
unchanged; there is nothing here to re-measure. What the release adds is the rule set an outside
|
||||
contributor could not previously read — including the reason the forge surface is shaped the way
|
||||
it is.
|
||||
|
||||
### Added
|
||||
|
||||
- `CONVENTIONS.md` — the whole rule set a change here is held to, consolidated. **Not new
|
||||
policy:** the charter lives in `CLAUDE.md`, the versioning and vendoring rules in `README.md`,
|
||||
the reporting route in `SECURITY.md`, and the file conventions were visible only in the shape
|
||||
of the files. Collected because a convention that exists only in the maintainer's head is not
|
||||
one an outside reader can meet.
|
||||
|
||||
Two things in it were previously inferable at best. **Why pull requests are off:** this
|
||||
repository is vendored into independent runtimes that pin a tag, so a change to detection data
|
||||
changes what they *find*, and that has to be coordinated with each consumer **before it
|
||||
exists** — which a merge button cannot do. `org-ops` reached that conclusion on 2026-08-11
|
||||
from a README line, and the conclusion was right; this file is the ground it was missing.
|
||||
**When a value may change:** the three mechanisms that have moved one so far — re-extraction,
|
||||
retraction, and owner-directed authoring — each named with the `source_fidelity` key that
|
||||
records it, and merit named explicitly as *not* on that list.
|
||||
|
||||
It also carries the four offline checks that stand in for the CI this organisation does not
|
||||
have. Each was confirmed to go **red** on a violation, not merely green on a clean tree: a
|
||||
JSON file with no `version`, a `spec/` file with no normative marker, and a planted `.sh` were
|
||||
each detected. A check that cannot fail proves nothing. The checks are shell one-liners rather
|
||||
than a script because a script would be `.sh`, and check 4 would fail on the tooling meant to
|
||||
enforce it.
|
||||
|
||||
- `README.md` — a short **Contributing** section pointing at it, carrying the pull-request answer
|
||||
inline so a reader who never opens the file still gets it. Same pattern the
|
||||
**Reporting a wrong entry** section followed for `SECURITY.md` in `0.3.1`.
|
||||
|
||||
The four `v0.4.1` references in the install block and the layout table move to `v0.4.2`.
|
||||
|
||||
This closes the second half of what `org-ops` recorded as missing against the org standard on
|
||||
2026-08-11. `SECURITY.md` was the first half, in `0.3.1`.
|
||||
|
||||
## [0.4.1] — 2026-08-11
|
||||
|
||||
**No data file changed and no pattern moved. A number this repository published was wrong, and
|
||||
it was wrong in our favour's opposite direction — the corrected figures are larger.** A runtime
|
||||
that passes `0.4.0` passes `0.4.1` unchanged.
|
||||
|
||||
### Fixed
|
||||
|
||||
- `docs/lexicon-port-divergence.md` (informative) — the ReDoS figures for
|
||||
`hybrid-xss:iframe-src` read **~3× low**, and the Python `script-tag` figure at 32 000 chars
|
||||
read ~4× low. Flagged by `llm-ingestion-pipeline-security` (coord, 2026-08-11T19:51:55Z), who
|
||||
measured the row themselves rather than citing ours.
|
||||
|
||||
Their diagnosis was measurement surface — their composed `scan_lexicon()` against our
|
||||
standalone regex. **Checked, and that is not the cause:** our standalone 100 000-char figure
|
||||
(7.86 s) sits close to their composed 8.95 s, so the two surfaces differ by far less than the
|
||||
error. Re-measured standalone, Python 3.14.0: `iframe-src` `[^>]*` is 822.7 ms at 32 000 chars
|
||||
and 51 477.4 ms at 256 000, against the published 119.6 ms and 16 857 ms. The Python
|
||||
`script-tag` figure at 256 000 chars *does* reproduce (5.44 s published, 5.22 s measured); the
|
||||
one at 32 000 chars does not (0.021 s against 0.087 s).
|
||||
|
||||
The error ratios are not constant, so a single mis-sized input does not explain it, and the
|
||||
original harness lived in a previous session's scratchpad and no longer exists. **The cause is
|
||||
recorded as not diagnosable rather than guessed at.** The correction is a box in the document
|
||||
carrying the re-measured table, and the superseded figures are struck in place rather than
|
||||
quietly overwritten — a consumer who cited the old number needs to find out that they did.
|
||||
|
||||
Nothing about the `0.4.0` decision depends on this. Every corrected figure is larger, the
|
||||
shape is unchanged (quadratic, ×4 per doubling), and both `[^><]*` forms remain flat under
|
||||
both engines. The `0.4.0` entry below still quotes the old `iframe-src` figure; it is left as
|
||||
published, because that section is the record of what was released.
|
||||
|
||||
## [0.4.0] — 2026-08-11
|
||||
|
||||
**Two detection values changed, by two different mechanisms, and the difference between those
|
||||
mechanisms is the point of the release.** One pattern table was **re-extracted** from a pinned
|
||||
upstream commit, the way every value in this repository has moved until now. Two lexicon rows were
|
||||
**authored here at the source owner's direction**, which has never happened before and required a
|
||||
reason that is not "we measured it and we were right."
|
||||
|
||||
A runtime that vendors this repository will see findings change. Any consumer asserting
|
||||
byte-identity against `v0.3.0` goes red by construction — `lexicon/injection-lexicon.json` changed
|
||||
pattern text. Ids, labels, aliases, family membership, case ids and every count are unchanged.
|
||||
|
||||
### Changed
|
||||
|
||||
- `lexicon/injection-lexicon.json` `0.7.0` → `0.8.0` — **`hybrid-xss:script-tag` and
|
||||
`hybrid-xss:iframe-src` narrow their unbounded negated class from `[^>]*` to `[^><]*`.** Both
|
||||
forms are quadratic in scan length on input that repeats the tag prefix and never supplies a
|
||||
`>`: each occurrence is a match start and `[^>]*` scans to end of input from each one. Measured
|
||||
in Node v25.8.2 at 16k / 32k / 64k / 128k / 256k chars — script-tag 32.65 / 113.36 / 479.02 /
|
||||
1988.83 / **7772.25** ms, iframe-src 39.23 / 131.76 / 574.94 / 2469.55 / **9449.94** ms, ×4 per
|
||||
doubling for both. Under `[^><]*` the same inputs cost 0.08–0.66 ms and 0.10–1.00 ms: flat, not
|
||||
merely faster.
|
||||
|
||||
**Why this is not commons correcting seed data.** The dependency direction inverted. As of
|
||||
`llm-security` `be14867` their four injection tables are built from this file and hold zero local
|
||||
regex literals — measured on their published surface at `47905da`, with their vendored copy of
|
||||
the lexicon confirmed byte-identical to `0.7.0`. So re-extraction was not available as a
|
||||
mechanism: there is no upstream literal left to re-read. They re-measured the finding rather than
|
||||
accepting it, rejected `[^>]{0,256}` because a bound is paddable and `[^>]{1,256}` because it
|
||||
drops bare `<script>` and two corpus cases with it, chose `[^><]*`, and asked commons to carry
|
||||
it. Recorded in a new `source_fidelity.owner_directed_changes` block — deliberately **not** in
|
||||
`post_extraction_drift`, which would have said the source moved and commons followed, when the
|
||||
source now reads commons.
|
||||
|
||||
Not decided by majority. The guard reached `[^><]` first and independently (`cff0437`), so all
|
||||
three runtimes now agree, but a 3-of-3 count is not what moved this value and would not have
|
||||
been sufficient. The justification is the same one that kept commons on `[^>]` through `0.7.0`:
|
||||
this file tracks its declared source, and the declared source chose.
|
||||
|
||||
Accepted cost, stated plainly: content carrying a literal `<` between the tag name and the `>`
|
||||
(`<script <x>`, `<script<div>`) stops matching. Measured across **all 90** conformance cases,
|
||||
not only the four that cite these ids: zero lost a match, zero gained one. The dropped class is
|
||||
real and unwitnessed by the corpus.
|
||||
|
||||
- `signatures/secret-egress.json` `0.2.0` → `0.3.0` — **the one-entry staleness disclosed in
|
||||
`0.2.0` is closed by re-extraction, 18 → 19 patterns.** `OpenAI Legacy API Key`
|
||||
(`\bsk-[A-Za-z0-9]{20}T3BlbkFJ[A-Za-z0-9]{20}\b`) enters at `order` 17, second to last; `JWT
|
||||
(three-part token)` moves to 18 and stays last, which `ordering.last_entry_is_load_bearing`
|
||||
requires.
|
||||
|
||||
Read out of the module text at a pinned public commit, never transcribed from the coord message
|
||||
that reported it — the message contained the regex, and that is exactly the path
|
||||
`evidence_limits` had ruled out. `refs/heads/main` on the public remote is `47905da`; `088e458`
|
||||
carries the entry and was confirmed an ancestor with `git merge-base --is-ancestor` rather than
|
||||
read off their log.
|
||||
|
||||
A side effect worth more than the entry: **all 19 positions were compared against the module** —
|
||||
name, regex source, flags, order — with 0 divergences. Positions 0–16 came from a 2026-08-09
|
||||
transcription whose module fidelity had stood recorded as llm-security's assertion rather than a
|
||||
reproduced result. It is now reproduced, and that `evidence_limits` bullet is retired along with
|
||||
the staleness one.
|
||||
|
||||
- `conformance/manifest.json` `0.3.1` → `0.3.2` — the `secret-egress` blocker prose said its note
|
||||
would stand until the re-extraction landed. It landed, so item (2) (`openai-api-key-legacy` is a
|
||||
real hole here) is marked closed, and the entry count moves 18 → 19. **The blocker itself does
|
||||
not close**: 19 entries against the guard's 25 at different cut points is still a table
|
||||
reconciliation nobody has performed, and one closed hole is not that reconciliation.
|
||||
|
||||
- `docs/lexicon-port-divergence.md` (informative) — the `[^>]` vs `[^><]` row gets the number
|
||||
it never had for **this** side, and then gets closed. The guard disclosed that their `[^><]` is a
|
||||
measured ReDoS fix (`cff0437`), not a preference, and asked commons to measure its own form
|
||||
rather than take their word. Measured here in Node and Python: `<script\b[^>]*>` is **quadratic**
|
||||
in scan position on input that denies it a `>` — ×4.0 per doubling, 6.7 s at 256 000 chars
|
||||
against 0.41 ms for the guard's form.
|
||||
|
||||
Counted across the whole lexicon rather than assumed from the one row: 8 of 83 patterns carry
|
||||
a bare `[^>]`, six of them bounded (`{1,256}`, measured linear) and **two unbounded**. The
|
||||
second unbounded one, `hybrid-xss:iframe-src`, had not been named by any party — 16.9 s at
|
||||
256 000 chars.
|
||||
|
||||
The finding was routed to the owning runtime, which is what `SECURITY.md` promises an outsider
|
||||
would happen — the first time that route was walked from the inside. It came back as a decision
|
||||
the same day, and the document now records the closure with the invariant intact: the
|
||||
measurement travelled, the authority did not. Had `llm-security` declined, both rows would still
|
||||
read `[^>]*` and this file would record a permanent divergence instead.
|
||||
|
||||
## [0.3.1] — 2026-08-11
|
||||
|
||||
**No pattern changed value. One shipped table is disclosed as stale, and the repository gains
|
||||
the reporting route it did not have.** Nothing in `patterns`, `expected.json` or any id moved,
|
||||
so a runtime that passes `0.3.0` passes `0.3.1` unchanged. Read the first entry anyway if you
|
||||
vendor `signatures/secret-egress.json`: it now says, in the file, that it under-matches its own
|
||||
source by one entry.
|
||||
|
||||
### Added
|
||||
|
||||
- `SECURITY.md` — the reporting route for a repository whose attack surface is **data**. It
|
||||
answers the question an ordinary security policy does not have to: how to report that a
|
||||
*detection-table entry is wrong*, and why a confirmed defect in extracted data is decided in
|
||||
the runtime it was extracted from before it is changed here. Names what is in scope (a silent
|
||||
false negative, a fixture that sanctions a miss, an unsafe normative clause, a secret in the
|
||||
history, data gone stale against its source), what is a documented boundary rather than a
|
||||
vulnerability, and the two classes that skip the routing — a real secret, and data authored
|
||||
here rather than extracted. States plainly that fix latency is bounded by the owning runtime's
|
||||
schedule and the consumer's pull, not by this repository's.
|
||||
|
||||
Written because `org-ops` recorded the file as missing against the org standard on
|
||||
2026-08-11, and because four files here are detection data where a mistake is a detector that
|
||||
looks like it works. `CONVENTIONS.md`, recorded in the same message, is not in this release.
|
||||
|
||||
- `README.md` — a short **Reporting a wrong entry** section pointing at it. Without it the
|
||||
policy is a file nobody looking at the front page would know to open.
|
||||
|
||||
### Changed
|
||||
|
||||
- `signatures/secret-egress.json` `0.1.0` → `0.2.0` — **a staleness disclosure, not a data
|
||||
change.** All 18 patterns are byte-identical to `0.1.0`; one entry is added to
|
||||
`provenance.evidence_limits`. `llm-security` reports having taken the source `SECRET_PATTERNS`
|
||||
from 18 to 19 by adding `OpenAI Legacy API Key`. That is recorded as their report and
|
||||
explicitly **not** reproduced here — the commit carrying it is not on their public remote,
|
||||
which was measured at `b1ba1fb` on 2026-08-11. What *was* measured here: none of the 18
|
||||
patterns matches a legacy `sk-…T3BlbkFJ…` key shape. So a consumer vendoring this file
|
||||
under-matches the seed hook by one entry, on a live credential shape, and now reads that in the
|
||||
file rather than inferring it. It will be closed by re-extraction from a pinned public commit,
|
||||
never by authoring the entry here from a message.
|
||||
|
||||
- `conformance/manifest.json` `0.3.0` → `0.3.1` — the `scope_planned.blockers` text for
|
||||
`signatures/secret-egress.json` is corrected. Through `0.3.0` it ended by naming
|
||||
`gcp-service-account-json` and `openai-api-key-legacy` together as ids "absent here". They are
|
||||
two different kinds of fact, and one of them was misleading.
|
||||
|
||||
Measured 2026-08-11, by running this file's own 18 patterns in `order` over a service-account
|
||||
document, against the guard at commit `e671edb`: a **complete** GCP service-account key file
|
||||
*is* matched here, at order 11 (`Private Key PEM Block` — its `(?:RSA |EC |DSA |OPENSSH )?`
|
||||
prefix group is optional, so the bare PKCS#8 header such a file carries matches). The same
|
||||
document with `private_key` removed matches nothing here while the guard's marker pattern still
|
||||
fires. That is a **cut-point** difference — the guard detects the document marker, this table
|
||||
detects the key material — which is what the blocker is about, and not a missing entry.
|
||||
`openai-api-key-legacy`, by contrast, is a real hole here today, and is now recorded as one.
|
||||
|
||||
The correction is folded into the existing blocker string rather than added as a sibling key:
|
||||
`blockers` is a map from table path to text, and a second key under a table path would read as
|
||||
a second table to anything iterating it.
|
||||
|
||||
- `docs/lexicon-port-divergence.md` (informative) — the residual `[^>]` vs `[^><]` row gains a
|
||||
fuller witness set. `llm-security` measured the three forms as **totally ordered** by what they
|
||||
match, each a strict superset of the next, and named two input classes the guard's narrower
|
||||
class drops (`<script a="<" >x`, `<script<div>`) beyond the one commons had recorded.
|
||||
Reproduced here independently, including the strict-superset property in both directions,
|
||||
before being written down.
|
||||
|
||||
Their argument — that the narrower class buys an empty false-positive surface, since anything
|
||||
reaching `[^>]`-and-not-`[^><]` already contains a literal `<script` tag — is recorded as
|
||||
theirs and explicitly **not** what decided commons' form. Commons carries `[^>]` on provenance,
|
||||
and would have carried `[^><]` had the source been the guard's. Also records that they asked to
|
||||
hear the guard's reason for `[^><]` before commons shipped and commons shipped first, with why
|
||||
that order was deliberate.
|
||||
|
||||
No data file touched; `v0.3.0` is unaffected.
|
||||
|
||||
## [0.3.0] — 2026-08-11
|
||||
|
||||
|
|
|
|||
199
CONVENTIONS.md
Normal file
199
CONVENTIONS.md
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
# Conventions
|
||||
|
||||
The rules a change to this repository is held to, in one place.
|
||||
|
||||
Nothing here is new policy. Every rule below was already being applied — some of it stated in
|
||||
[README.md](README.md), some in [SECURITY.md](SECURITY.md), some only visible in the shape of
|
||||
the files themselves. It is collected here because a convention that only exists in the
|
||||
maintainer's head is not a convention an outside reader can meet, and because two of the
|
||||
decisions this repository makes — that nothing here runs, and that pull requests are switched
|
||||
off — look arbitrary until the reason is written down next to them.
|
||||
|
||||
This file binds **contributions to this repository**. It does not bind the runtimes that read
|
||||
the data; that is what `spec/` is for, and those files say `Status: normative` and mean it.
|
||||
|
||||
## The charter: nothing here runs
|
||||
|
||||
**This repository contains no executable code, and it will not acquire any.**
|
||||
|
||||
Not permitted, without exception:
|
||||
|
||||
- `.mjs`, `.js`, `.ts`, `.py`, `.sh` — or any other file that implements detection, scanning,
|
||||
normalisation, scoring or I/O;
|
||||
- `package.json`, `pyproject.toml`, lockfiles, dependencies, build steps;
|
||||
- an import of, or knowledge of, any framework, SDK or runtime;
|
||||
- network access, model calls, dependence on the clock, or randomness.
|
||||
|
||||
Permitted: JSON data, normative specifications in Markdown, and conformance fixtures
|
||||
(`input.txt` plus `expected.json`).
|
||||
|
||||
The reason is `fork-and-own`. A consumer on a runtime nobody here has thought of should be able
|
||||
to vendor this repository without inheriting a single technology choice. A build step is a
|
||||
technology choice; so is a test runner. The moment one exists, the set of runtimes that can
|
||||
adopt this core shrinks to the set that tolerates it.
|
||||
|
||||
The consequence is that **this repository cannot validate itself**. There is no CI in this
|
||||
organisation and nothing runs on push. The checks below are yours to run, and they are the only
|
||||
ones there are.
|
||||
|
||||
## How a change gets in — and why not by pull request
|
||||
|
||||
Pull requests are switched off on the canonical repository at
|
||||
`git.fromaitochitta.com/open/llm-security-commons`, and issues are not the reporting channel
|
||||
either. That is deliberate, and the reason is stronger than a preference about tooling.
|
||||
|
||||
This repository is **vendored into independent runtimes** — a Claude Code plugin on Node/ESM, a
|
||||
Python guard, an advisory surface — each pinning a tag. The contract between them is semver, and
|
||||
a change to detection data changes what those runtimes *find*. A patch to a pattern table is not
|
||||
a contribution that can be merged and then socialised; it is a contract change that has to be
|
||||
coordinated with every consumer **before it exists**, because the moment it is tagged, the next
|
||||
consumer to pull it gets different findings than the one that pulled yesterday. A merge button
|
||||
does not do that, and nothing downstream of a merge button can.
|
||||
|
||||
So the routes in are:
|
||||
|
||||
1. **Fork and own it.** MIT, and an intended use rather than a tolerated one. If you need a
|
||||
different value in your runtime, this is the fast path and it is fully supported.
|
||||
2. **Report it privately** — see [SECURITY.md](SECURITY.md). A wrong entry in a detection table
|
||||
is a silent false negative in every runtime that reads it, so a report about one is a
|
||||
security report even though nothing here executes. That file also explains why a confirmed
|
||||
defect in extracted data is usually decided in the runtime it came from before it changes
|
||||
here.
|
||||
|
||||
If you maintain a consumer, the coordination channel is direct contact with the maintainer, not
|
||||
this repository's forge surface.
|
||||
|
||||
## Data files (JSON)
|
||||
|
||||
- **Every JSON file carries a top-level `"version"`** — a semver string. No exceptions.
|
||||
- **Every JSON file states what it is and where its data came from**, in a top-level
|
||||
`"$comment"` or `"description"`. Provenance is not optional metadata here; it is what makes
|
||||
the difference between a table and a rumour.
|
||||
- 2-space indentation, LF line endings, a trailing newline, UTF-8 without BOM.
|
||||
- **Code points are written as strings** — `"U+200B"` — never as the raw invisible character.
|
||||
Review cannot see what it cannot render, and a reviewer who cannot see a character cannot
|
||||
check it. The single exception is `conformance/*/input.txt`, which by definition contains the
|
||||
actual bytes.
|
||||
- **Keys are stable identifiers.** Consumers match on them. **Changing a key is a breaking
|
||||
change** and is versioned as one.
|
||||
|
||||
The four files that carry detection material — `codepoints/carriers.json`,
|
||||
`signatures/secret-egress.json`, `signatures/malware-signatures.json`,
|
||||
`signatures/active-content.json` — take one further rule, which is the most important line in
|
||||
this document:
|
||||
|
||||
> **They are changed only from verified source data** — a dump from the owning repository, the
|
||||
> Unicode standard, published vendor documentation. Never from memory, never "filled in for
|
||||
> consistency". A guessed code point or a regex with wrong escaping is a silent false negative:
|
||||
> a detector that looks like it is working and is not looking. If an entry cannot be verified,
|
||||
> leave it out, or mark it explicitly unverified in its `$comment`.
|
||||
|
||||
## Specifications (Markdown)
|
||||
|
||||
- A normative specification carries **`Status: normative`** at the top and uses RFC 2119 terms
|
||||
(MUST / MUST NOT / SHOULD / MAY) in uppercase, deliberately. These files bind the
|
||||
implementations that read them.
|
||||
- An informative document (`docs/`) carries **`Status: informative`** and is **never ground
|
||||
truth**. It records measurements, history and open disagreements; a runtime is not wrong for
|
||||
disagreeing with one.
|
||||
- Naming a file that does not exist yet is allowed where the layout is part of the contract —
|
||||
`spec/decode-pipeline.md` is named in README.md and marked **Planned**. It is not a link, and
|
||||
nothing depends on it. A normative spec guessed at would be worse than an absent one.
|
||||
|
||||
## Conformance cases
|
||||
|
||||
- One directory per case: `conformance/<case-id>/input.txt` and
|
||||
`conformance/<case-id>/expected.json`.
|
||||
- `<case-id>` is stable and descriptive. **Changing a case id is a breaking change** — a
|
||||
published conformance result names it.
|
||||
- `expected.json` is **ground truth**. If a runtime disagrees with it, the runtime is wrong.
|
||||
- The one way that reverses: the fixture is proven wrong. Then the fixture changes **in its own
|
||||
commit, with the reason written down** — never folded into a change that does something else,
|
||||
because a fixture edit is the one edit that can make every conforming runtime wrong
|
||||
identically.
|
||||
- A case declares the data files it is `scope`d to. A runtime that does not implement a scoped
|
||||
table reports the case `not-applicable` — a third verdict beside pass and fail, and one that
|
||||
must be reported rather than dropped from the denominator. See
|
||||
[`spec/conformance-corpus.md`](spec/conformance-corpus.md).
|
||||
|
||||
## When a value may change
|
||||
|
||||
Detection values do not change here because someone here judged them wrong. Three mechanisms
|
||||
have moved a value so far, and each is recorded in the file itself rather than only in the
|
||||
changelog:
|
||||
|
||||
1. **Re-extraction** — the owning runtime changed its own value, and this repository re-read the
|
||||
source at a pinned public commit. Recorded in `source_fidelity.post_extraction_drift`.
|
||||
2. **Retraction** — this repository described its own provenance wrongly. The stored value may
|
||||
have been right all along; the account of where it came from was not. Recorded in
|
||||
`source_fidelity.retracted`.
|
||||
3. **Owner-directed authoring** — the owning runtime decided a value and asked this repository
|
||||
to carry it, because the dependency has inverted: the source now reads *this* file and holds
|
||||
no literal to re-read. Recorded separately, in `source_fidelity.owner_directed_changes`,
|
||||
precisely because calling it drift would assert that the source moved and commons followed —
|
||||
which would be false in the one direction that matters.
|
||||
|
||||
What is **not** on that list is merit. Data extracted from an implementation is kept
|
||||
behaviour-identical to it on purpose, because a copy that disagrees with its source is the exact
|
||||
failure this repository exists to prevent. Producing one as a *fix* would be self-defeating. If
|
||||
you believe an extracted value is wrong, say so — and expect the decision to be taken in the
|
||||
runtime where the pattern is under test.
|
||||
|
||||
Data **authored here** rather than extracted — conformance payloads, flagged as
|
||||
`authored_payloads` in `conformance/manifest.json` — is this repository's own to correct.
|
||||
|
||||
## Versioning
|
||||
|
||||
Two version numbers, and they are not locked to each other:
|
||||
|
||||
- **The repository tag** (`vX.Y.Z`) versions **the contract**: the file set, the key names, the
|
||||
case ids, the disposition semantics.
|
||||
- **Each JSON file's own `"version"`** is bumped when *that file* changes.
|
||||
|
||||
What counts as which:
|
||||
|
||||
| Change | Bump |
|
||||
| --- | --- |
|
||||
| New data field, new entry | minor |
|
||||
| Changed or removed key, case id, or layout | **major** — consumers break |
|
||||
| A change to what a conforming runtime *finds* | minor in 0.x, and the changelog says so |
|
||||
|
||||
That last row is why **the changelog entry is the thing to read before upgrading, not the
|
||||
version number**. Pre-1.0, a release that changes findings is still a minor bump; only the entry
|
||||
tells you whether your assertions move.
|
||||
|
||||
Consumers vendor **a tag, never `main`** — a conformance result can only be attributed to a tag.
|
||||
Nothing polls for updates; when a change moves detection data, the maintainer notifies known
|
||||
consumers directly, and their upgrade remains their own action on their own schedule.
|
||||
|
||||
## Checks to run before proposing a change
|
||||
|
||||
There is no CI. These four are the validation surface, they run offline in a second, and each
|
||||
one has been confirmed to go red on a violation rather than merely green on a clean tree.
|
||||
|
||||
```bash
|
||||
# 1. Every JSON file is well-formed
|
||||
find . -name '*.json' -not -path './.git/*' -print0 \
|
||||
| xargs -0 -n1 python3 -m json.tool > /dev/null && echo OK
|
||||
|
||||
# 2. Every JSON file outside conformance/ carries a top-level "version"
|
||||
find . -name '*.json' -not -path './.git/*' -not -path './conformance/*' -print0 \
|
||||
| xargs -0 python3 -c 'import json,sys
|
||||
missing=[p for p in sys.argv[1:] if "version" not in json.load(open(p))]
|
||||
print("\n".join("MISSING version: "+p for p in missing) or "OK")'
|
||||
|
||||
# 3. Every spec carries its normative marker (prints offending files, nothing = clean)
|
||||
grep -L 'Status: normative' spec/*.md || echo OK
|
||||
|
||||
# 4. Charter guard: no executable code has crept in
|
||||
find . -type f \( -name '*.mjs' -o -name '*.js' -o -name '*.ts' -o -name '*.py' -o -name '*.sh' \) \
|
||||
-not -path './.git/*' | grep . && echo 'CHARTER VIOLATION' || echo OK
|
||||
```
|
||||
|
||||
They are written as shell one-liners rather than shipped as a script because a script would be
|
||||
`.sh`, and check 4 would then fail on the tooling meant to enforce it.
|
||||
|
||||
What they do **not** check: whether a value is *correct*. Nothing offline can. That is what the
|
||||
conformance corpus is for, and it runs in each consumer's own test suite against a pinned tag —
|
||||
constraining two of the seven data files, which is a real limit and is stated in
|
||||
[README.md](README.md) under **Known limitations**.
|
||||
40
README.md
40
README.md
|
|
@ -24,7 +24,7 @@ As a `git subtree` (recommended: history is preserved and upgrades are a single
|
|||
|
||||
```bash
|
||||
git subtree add --prefix vendor/commons \
|
||||
https://git.fromaitochitta.com/open/llm-security-commons.git v0.3.0 --squash
|
||||
https://git.fromaitochitta.com/open/llm-security-commons.git v0.4.3 --squash
|
||||
|
||||
# later, to move to a newer tag
|
||||
git subtree pull --prefix vendor/commons \
|
||||
|
|
@ -34,7 +34,7 @@ git subtree pull --prefix vendor/commons \
|
|||
Or pin a tag and copy — `fork-and-own` is an explicitly supported path:
|
||||
|
||||
```bash
|
||||
git clone --depth 1 --branch v0.3.0 \
|
||||
git clone --depth 1 --branch v0.4.3 \
|
||||
https://git.fromaitochitta.com/open/llm-security-commons.git
|
||||
```
|
||||
|
||||
|
|
@ -61,14 +61,14 @@ and keeping it that small is the point.
|
|||
| [`schema/conformance-declaration.schema.json`](schema/conformance-declaration.schema.json) | **Normative.** The shape a runtime publishes alongside a conformance result: which commons tables it implements, the commons commit it measured, and the four verdict counts. Required by the corpus spec §1.1; not validated by anything here, because nothing here runs. |
|
||||
| [`spec/conformance-corpus.md`](spec/conformance-corpus.md) | **Normative.** How to read the corpus: what a case is, why `input.txt` is bytes rather than text, what `exact-within-scope` requires of a runtime, and how a runtime declares its table set so a case scoped outside it reads as `not-applicable` rather than as a failure. |
|
||||
| [`conformance/`](conformance/) | 90 cases. One directory per case: `input.txt` in, `expected.json` out. Ground truth. 84 cover the injection lexicon — 83 one per pattern, both seeding runtimes measured producing the same verdict on all 83, plus one variant case gating a pattern form against its predecessor. Six cover active content and are measured against the one runtime that implements that table — `not-applicable` for the other, not failing. See [`conformance/manifest.json`](conformance/manifest.json). |
|
||||
| `spec/decode-pipeline.md` | **Planned, still not shipped as of v0.3.0.** The decode order, in RFC 2119 language. Two runtimes that decode in different orders will disagree on identical input. Writing it needs the decode implementation, which is engine code and has not been supplied — and a normative spec guessed from a data dump would be worse than an absent one. |
|
||||
| `spec/decode-pipeline.md` | **Planned, still not shipped as of v0.4.3.** The decode order, in RFC 2119 language. Two runtimes that decode in different orders will disagree on identical input. Writing it needs the decode implementation, which is engine code and has not been supplied — and a normative spec guessed from a data dump would be worse than an absent one. |
|
||||
| [`docs/extraction-plan.md`](docs/extraction-plan.md) | Informative: where each file was seeded from, and what v0.1.0 promised. |
|
||||
| [`docs/lexicon-port-divergence.md`](docs/lexicon-port-divergence.md) | Informative: a measured disagreement between two ports of the injection lexicon — 13 patterns that behave differently, in both directions, and why no data file was changed because of it. |
|
||||
| [`docs/lexicon-port-divergence.md`](docs/lexicon-port-divergence.md) | Informative: a measured disagreement between two ports of the injection lexicon — 13 patterns that behave differently, in both directions. Most of it is still open, and the two rows that closed in v0.4.0 closed because the runtime that owns the value decided, not because this document found them wrong. |
|
||||
|
||||
Every JSON file carries a top-level `version`. Every normative specification carries a
|
||||
`Status: normative` marker. Rows marked **Planned** are named here because the layout is
|
||||
part of the contract, but the file does not exist yet — they are not links, and nothing in
|
||||
v0.3.0 depends on them.
|
||||
v0.4.3 depends on them.
|
||||
|
||||
Each data file records its own provenance and, in `verified`, how strongly it is backed.
|
||||
`calibration/calibration.json` is currently the one file that says `false`: it was
|
||||
|
|
@ -102,10 +102,12 @@ number would misreport both:
|
|||
`not-applicable` says the corpus did not ask, never that the runtime is blind.
|
||||
|
||||
Four cases remain unshipped, for the carrier and secret-egress tables, and neither is
|
||||
blocked on effort. Carriers has no adoptable id space: one runtime labels the same carrier
|
||||
differently depending on pipeline stage, which would make a case's verdict depend on the
|
||||
entry point it was measured through. Secret egress is not an id question at all — the two
|
||||
runtimes carry *different tables*, 18 entries against 25, cut at different granularities.
|
||||
blocked on effort. Carriers has no *ratified* id space: six labels exist to adopt verbatim
|
||||
from one runtime, but adopting them would hand the other an id asserting a strip it does not
|
||||
perform, and publishing a single alias would force that runtime's declared table set to widen
|
||||
— turning three cases it cannot reach from `not-applicable` into failures. Put to both
|
||||
runtimes as a decision request on 2026-08-11; unanswered. Secret egress is not an id question at all — the two
|
||||
runtimes carry *different tables*, 19 entries against 25, cut at different granularities.
|
||||
`conformance/manifest.json` records both blockers under `scope_planned.blockers`, measured,
|
||||
so the gap is visible rather than inferred.
|
||||
|
||||
|
|
@ -150,6 +152,26 @@ so the gap is visible rather than inferred.
|
|||
- **The homoglyph map is finite.** Confusable coverage is a long tail; absence from the map
|
||||
is not evidence a character is safe.
|
||||
|
||||
## Contributing
|
||||
|
||||
[CONVENTIONS.md](CONVENTIONS.md) is the whole rule set a change here is held to: the charter
|
||||
(nothing runs, and why that is load-bearing rather than fussy), the file conventions, when a
|
||||
detection value is allowed to move, how the two version numbers work, and the four offline
|
||||
checks that stand in for the CI this organisation does not have.
|
||||
|
||||
It also answers the question the forge surface raises on its own: **pull requests are switched
|
||||
off, deliberately.** This repository is vendored into independent runtimes that pin a tag, so a
|
||||
change to detection data changes what they *find* — that has to be coordinated with each
|
||||
consumer before it exists, which a merge button cannot do. Fork-and-own is the supported path;
|
||||
a wrong entry is reported privately.
|
||||
|
||||
## Reporting a wrong entry
|
||||
|
||||
A wrong code point or a mis-escaped regex here is a silent false negative in every runtime
|
||||
that reads it, so it is a security report even though nothing runs. Send it privately — see
|
||||
[SECURITY.md](SECURITY.md), which also explains why a confirmed defect in extracted data is
|
||||
decided in the runtime it came from before it is changed here.
|
||||
|
||||
## Changelog
|
||||
|
||||
See [CHANGELOG.md](CHANGELOG.md).
|
||||
|
|
|
|||
133
SECURITY.md
Normal file
133
SECURITY.md
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
# Security policy
|
||||
|
||||
This repository ships **no runnable code** — no package, no build, no dependency tree,
|
||||
nothing that executes on your machine. So the usual question, *can this be exploited*,
|
||||
has an unusual answer here: the attack surface is the **data**.
|
||||
|
||||
Seven data files here carry the detection material — pattern tables, code-point carriers,
|
||||
calibration thresholds, an OWASP mapping — and several independent runtimes read them at the
|
||||
same time. A wrong code point, a mis-escaped regex, a fixture that expects a miss: none of that
|
||||
crashes anything. It produces a detector that looks like it works and is not looking. That
|
||||
is the vulnerability class this policy is about, and a report of one is welcome even though
|
||||
no code changes as a result.
|
||||
|
||||
## Reporting
|
||||
|
||||
**Do not open a public issue.** A report here usually names an input that gets *past* a
|
||||
detector, and that is a working bypass against every consumer until it is closed.
|
||||
|
||||
Report privately by email:
|
||||
|
||||
- **hello@fromaitochitta.com**, with `SECURITY` at the start of the subject.
|
||||
|
||||
Pull requests are not the channel either — they are switched off on the canonical
|
||||
repository, and not as an oversight. This repository is vendored into independent runtimes
|
||||
that pin a tag; a change to detection data changes what those runtimes *find*. Such a change
|
||||
has to be coordinated with each consumer before it exists, which a merge button does not do.
|
||||
Fork-and-own is the supported path.
|
||||
|
||||
Please include:
|
||||
|
||||
- the file and the entry — its `name`, `order` or `id`, whichever that file uses;
|
||||
- the tag you read (`v0.3.0`, not "main");
|
||||
- the input that should have matched and does not, or the input that matches and should not;
|
||||
- what a consuming runtime actually does today, if you have measured it.
|
||||
|
||||
**Obfuscate live payloads.** Do not send a working credential or a live carrier. Spell
|
||||
invisible characters as code points the way the tables do (`"U+200B"`), and use placeholder
|
||||
key material — a report should not itself be a delivery mechanism.
|
||||
|
||||
## What counts as a vulnerability here
|
||||
|
||||
In scope — all of these are real reports:
|
||||
|
||||
1. **A detection entry that is a silent false negative.** A wrong code point, a regex whose
|
||||
escaping is wrong for the declared dialect, missing or wrong flags, a pattern that fails
|
||||
to compile in a documented engine and gets skipped rather than reported.
|
||||
2. **A conformance fixture that sanctions a miss.** `expected.json` is ground truth: a
|
||||
runtime that disagrees with it is deemed wrong. A fixture that expects too little makes
|
||||
every conforming runtime wrong identically, and the corpus will not catch it.
|
||||
3. **A normative clause that mandates unsafe behaviour.** The `spec/` files bind the
|
||||
implementations that consume them, so a weak rule propagates to all of them.
|
||||
4. **A real secret or personal data in the repository or its history.** The history is
|
||||
public in full.
|
||||
5. **Data that has gone stale against its declared source in a way that under-detects.**
|
||||
Each data file names its source in a `provenance` block. If that source has since added
|
||||
or corrected an entry, the copy here under-matches, and a consumer vendoring it is less
|
||||
protected than the runtime it was taken from.
|
||||
|
||||
Out of scope — documented boundaries, not vulnerabilities. See **Known limitations** and
|
||||
**Non-goals** in [README.md](README.md):
|
||||
|
||||
- a detection class absent from the tables entirely (coverage is the union of what the seed
|
||||
implementations detected, not of what exists);
|
||||
- a table implemented by only one runtime, and cases marked `not-applicable` for the others;
|
||||
- disagreement about a `calibration.json` threshold — the thresholds are published, the
|
||||
policy built on them belongs to the consumer;
|
||||
- a divergence already recorded in [`docs/lexicon-port-divergence.md`](docs/lexicon-port-divergence.md);
|
||||
- the five data files no fixture constrains, and the finite homoglyph map.
|
||||
|
||||
If you are unsure which side something falls on, report it privately anyway.
|
||||
|
||||
## Why a confirmed defect is usually not fixed here first
|
||||
|
||||
This is the part that differs from an ordinary repository, and it is worth reading before
|
||||
you conclude that a fix is being stalled.
|
||||
|
||||
Most data here is an **extraction**: a copy of a table that lives in a runtime, kept
|
||||
behaviour-identical to it on purpose. Correcting an entry here — even a genuinely wrong one
|
||||
— would make the copy disagree with the implementation it was taken from. Two implementations
|
||||
answering differently on the same input is precisely the failure this repository exists to
|
||||
prevent, so producing one as a *fix* would be self-defeating.
|
||||
|
||||
A confirmed defect in extracted data therefore travels:
|
||||
|
||||
1. the report reaches the maintainer here, privately;
|
||||
2. the owning runtime is identified — every data file names it in `provenance.source_repo`
|
||||
— and the report is routed there;
|
||||
3. the decision is taken **there**, where the pattern is under test against a real suite;
|
||||
4. once the source has moved, this repository **re-extracts** from a pinned public commit
|
||||
and tags a release;
|
||||
5. consumers pull that tag on their own schedule.
|
||||
|
||||
Stated plainly, because it affects you: fix latency is bounded by the owning runtime's
|
||||
schedule and by each consumer's pull, not by this repository's. If you need protection
|
||||
sooner than that, the fix belongs in your own runtime; this repository is where it becomes
|
||||
shared, not where it becomes real.
|
||||
|
||||
Two things do **not** take that route:
|
||||
|
||||
- **A real secret in the repository or its history** (class 4) is handled here, immediately.
|
||||
- **Data authored in this repository** rather than extracted — it is flagged as such where
|
||||
it occurs, for example `authored_payloads` in `conformance/manifest.json` — is this
|
||||
repository's own to correct.
|
||||
|
||||
The precedent is on the record. In `v0.3.0` a detection pattern changed value here for the
|
||||
first time, and it changed because the owning runtime had changed its own and this
|
||||
repository re-read the source — not because a reviewer here judged the old value wrong.
|
||||
`docs/lexicon-port-divergence.md` records a row where two runtimes still disagree and this
|
||||
repository deliberately did *not* pick a winner. Provenance is the ground for moving a
|
||||
value. Merit is not, and the day it becomes the ground, the guarantee is gone.
|
||||
|
||||
## Supported versions
|
||||
|
||||
Pre-1.0. Only the latest tag is fixed; there are no back-ported branches.
|
||||
|
||||
Consumers vendor this repository (`git subtree`, or a pinned copy) rather than installing
|
||||
it, so a fix reaches a consumer only when that consumer pulls the new tag. There is no CI in
|
||||
this organisation and nothing polls for updates. When a fix changes detection data, the
|
||||
maintainer notifies the known consumers directly — but their upgrade is their own action, on
|
||||
their own schedule.
|
||||
|
||||
Read the `CHANGELOG.md` entry before upgrading rather than the version number: in 0.x, a
|
||||
change to what a conforming runtime *finds* is still a minor bump.
|
||||
|
||||
## Disclosure
|
||||
|
||||
There is no formal embargo SLA here. The maintainer will acknowledge the report, agree a fix
|
||||
and disclosure timeline with the reporter, and credit the reporter in the `CHANGELOG.md`
|
||||
entry unless they prefer to remain anonymous.
|
||||
|
||||
If the report is a false negative in a table that has already shipped, the changelog entry
|
||||
will say what slipped through, in enough detail that a consumer still pinned to the older
|
||||
tag can judge whether it is exposed. Naming it is the point of fixing it.
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -5,6 +5,11 @@ records a measured disagreement between two ports of one source table, so the de
|
|||
taken where each table is tested. Under this repository's behaviour-preservation invariant,
|
||||
a divergence found here is **reported, not fixed**.
|
||||
|
||||
That invariant held through the closure recorded below. Two rows this document measured did change
|
||||
in `lexicon/injection-lexicon.json` `0.8.0` — and not because this document found them wrong. The
|
||||
finding was routed to the runtime that owns the value, that runtime re-measured it, decided, and
|
||||
asked commons to carry the result. The measurement travelled; the authority did not.
|
||||
|
||||
Produced 2026-08-09. Every number below came from a command; the scripts live in the session
|
||||
scratchpad rather than in this repository, because executable code here would breach the
|
||||
charter. They are reproducible from the method column.
|
||||
|
|
@ -166,18 +171,59 @@ The direction matters for what it cannot break: the new form matches a strict su
|
|||
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:
|
||||
**The one-byte span difference is CLOSED as of commons `0.8.0`** — all three runtimes now carry
|
||||
`[^><]*`. What follows is the account of the divergence while it was open, kept because the table
|
||||
below is the measurement that made the closure decidable, and because a consumer diffing against
|
||||
`v0.3.0` will find the two changed rows and needs the coordinate.
|
||||
|
||||
| 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** |
|
||||
While it was open, the guard excluded `<` from its negated class and the other two did not. The
|
||||
three forms turn out to be **totally ordered by what they match** — each a strict superset of the
|
||||
next:
|
||||
|
||||
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.
|
||||
| Witness | commons `0.7.0` / `llm-security` `[^>]` | guard `[^><]`, and commons from `0.8.0` | commons ≤ `0.6.0` |
|
||||
| --- | --- | --- | --- |
|
||||
| `<script>steal()</script>` | match | match | match |
|
||||
| `<script src=x.js>` | match | match | no match |
|
||||
| `<script>alert(1)` | match | match | no match |
|
||||
| `<script\n src=x>` | match | match | no match |
|
||||
| `<script a="<" >x` | **match** | **no match** | no match |
|
||||
| `<script<div>` | **match** | **no match** | no match |
|
||||
|
||||
Measured by compiling all three forms, not reasoned from the character classes. The last two rows
|
||||
are the whole residual divergence.
|
||||
|
||||
The witness set and the ordering observation are `llm-security`'s, from their coord message of
|
||||
2026-08-11T11:01:59Z; both were **reproduced here independently** before being recorded, including
|
||||
the strict-superset property in both directions. Their reading of it: `[^><]` refuses to cross a
|
||||
`<`, and since anything that reaches `[^>]`-and-not-`[^><]` already contains a literal `<script`
|
||||
opening tag, the narrower class buys an empty false-positive surface in exchange for two dropped
|
||||
input classes — a recall loss with no precision gain to trade for.
|
||||
|
||||
**That argument is recorded, not endorsed, and it did not decide commons' form.** Commons carries
|
||||
`[^>]` because the lexicon's declared provenance is `llm-security`'s table (see above), not
|
||||
because their regex won on merit — the same reasoning would have taken commons to `[^><]` had the
|
||||
source been the guard's. `llm-security` asked to hear the guard's reason for `[^><]` — a ReDoS
|
||||
bound, or a span-extraction consumer needing the run to stop at `<` — before commons shipped, and
|
||||
commons shipped first. That order is deliberate rather than an oversight: a reason that beats this
|
||||
argument is a reason for the *guard's own port*, and commons would still track its declared
|
||||
source. Put to the guard as an open question 2026-08-11; unanswered at time of writing.
|
||||
|
||||
**How it resolved, and why the resolution used the same rule rather than overturning it.** Both
|
||||
halves of that open question came back the same day. The guard answered that `[^><]` is a
|
||||
*measured* ReDoS fix on their side (their commit `cff0437`), not an aesthetic choice — so the
|
||||
"reason that beats this argument" the paragraph above asked for turned out to exist. And commons
|
||||
then measured its own copy and found the same defect in it: `<script\b[^>]*>` and
|
||||
`<iframe\b[^>]*src…` are both quadratic in scan length, the second one having been named by nobody
|
||||
until it was counted. `llm-security` re-measured rather than taking the report, reached `[^><]*`
|
||||
independently, and asked commons to carry it — because their table is now *built from this file*,
|
||||
so there is no local literal left for them to change without forking.
|
||||
|
||||
The paragraph above said commons "would still track its declared source." That is exactly what
|
||||
`0.8.0` does: the declared source chose `[^><]*` and commons followed. The rule did not bend to
|
||||
accommodate a better argument — the argument won *at the source*, which is the only place this
|
||||
repository lets an argument decide a `pattern` value. The recall cost stated in the two bold rows
|
||||
of the table is now paid deliberately: measured across all 90 conformance cases, zero lost a match
|
||||
and zero gained one, so the dropped classes remain real and remain unwitnessed by the corpus.
|
||||
|
||||
## Why they diverge: two different ReDoS mitigations of one table
|
||||
|
||||
|
|
@ -223,6 +269,98 @@ disagreement — not an oversight on either side.
|
|||
Neither strategy is free, and neither is obviously right. That is the decision the two owning
|
||||
repositories have to take, and it is not commons' to take for them.
|
||||
|
||||
### The cost of commons' strategy, measured here — 2026-08-11
|
||||
|
||||
The section above says neither strategy is free and neither is obviously right. That was
|
||||
written without a number for **this** side's cost. There is one now, and it is not symmetric
|
||||
with the guard's.
|
||||
|
||||
The prompt came from the guard (coord, 2026-08-11): their `[^><]` is not a preference but a
|
||||
measured ReDoS fix (`cff0437`, 2026-07-31), and they asked commons to measure its own form
|
||||
rather than take their word. Measured here — Node v25.8.2 and Python 3.14.0, Intel i9-9880H,
|
||||
patterns read out of `lexicon/injection-lexicon.json` and run over `'<script '` repeated,
|
||||
the unit that denies the pattern the `>` it needs:
|
||||
|
||||
| chars | `<script\b[^>]*>` (commons, Node) | `<script\b[^><]*>` (guard form, Node) |
|
||||
|---|---|---|
|
||||
| 16 000 | 25.5 ms | 0.08 ms |
|
||||
| 32 000 | 101.3 ms | 0.06 ms |
|
||||
| 64 000 | 413.5 ms | 0.12 ms |
|
||||
| 128 000 | 1 676.5 ms | 0.24 ms |
|
||||
| 256 000 | 6 681.6 ms | 0.41 ms |
|
||||
|
||||
×4.0 per doubling against ×2 — quadratic against linear, reproduced in Python `re` on the same
|
||||
inputs (0.021 s → 5.44 s over the same range, guard form 0.0001 s → 0.0015 s). It is
|
||||
scan-position quadratic, not exponential backtracking: `<script` matches at ~n positions and
|
||||
`[^>]*` scans to end of input at each one. Nothing here is catastrophic backtracking, and
|
||||
nothing here needs a nested quantifier to happen.
|
||||
|
||||
> **Correction, 2026-08-11 (commons `v0.4.1`) — two of the Python figures above read low, and
|
||||
> the `iframe-src` figure below read low by ~3×.** The guard flagged it (coord,
|
||||
> 2026-08-11T19:51:55Z) after measuring the `iframe-src` row themselves and getting roughly 4×
|
||||
> our published number. They attributed the gap to measurement surface — their composed
|
||||
> `scan_lexicon()` against our standalone regex. **That is not the explanation.** Re-measured
|
||||
> here standalone, the same surface the numbers above claim, Python 3.14.0, median-stable across
|
||||
> repeated runs (the two 256 000-char `iframe-src` runs landed 3 ms apart in 51 s):
|
||||
>
|
||||
> | chars | `script-tag` `[^>]*` | `script-tag` `[^><]*` | `iframe-src` `[^>]*` | `iframe-src` `[^><]*` |
|
||||
> |---|---|---|---|---|
|
||||
> | 32 000 | 87.4 ms | 0.17 ms | 822.7 ms | 0.25 ms |
|
||||
> | 100 000 | — | — | 7 859.5 ms | — |
|
||||
> | 256 000 | 5 222.6 ms | 1.25 ms | 51 477.4 ms | 2.08 ms |
|
||||
>
|
||||
> So: the Python `script-tag` figure at 256 000 chars **reproduces** (5.44 s published against
|
||||
> 5.22 s measured), and the one at 32 000 chars does **not** (0.021 s against 0.087 s). The
|
||||
> `iframe-src` pair below reproduces at neither point. The error ratios are not constant, so a
|
||||
> single mis-sized input does not explain it, and the original harness lived in a previous
|
||||
> session's scratchpad and is gone — **the cause is not diagnosable and is not guessed at here.**
|
||||
> Our standalone 100 000-char figure (7.86 s) sits close to the guard's composed 8.95 s, which
|
||||
> is why their surface hypothesis does not survive: the two surfaces differ by much less than
|
||||
> the error did.
|
||||
>
|
||||
> **What does not change:** the shape (quadratic, ×4 per doubling), the direction, and the
|
||||
> decision. Every corrected figure is *larger*, so the case against the old forms is stronger
|
||||
> than the one originally published, and the new forms are flat under both engines. The guard
|
||||
> flagged this because a low number could matter if either side later pins a bound — they were
|
||||
> right to, and the number they should pin against is the one in this box.
|
||||
|
||||
**Two of the 83 patterns have this shape, not one.** Counted over the whole file, 8 patterns
|
||||
carry a bare `[^>]` and none carries `[^><]`. Six of the 8 are the html-obfuscation family and
|
||||
are **bounded** (`[^>]{1,256}`) — measured linear (51 → 164 ms across a 8× input growth), so
|
||||
bounding does work as a mitigation, at the recall cost the section above already records. The
|
||||
other two are unbounded `[^>]*`:
|
||||
|
||||
- `hybrid-xss:script-tag` — the row this file already tracks.
|
||||
- `hybrid-xss:iframe-src` — **not previously named by anyone**, found here by counting rather
|
||||
than by being told. Same shape, same result: 119.6 ms → 16 857 ms over 32 000 → 256 000
|
||||
chars of `'<iframe '`. **Both figures are superseded — see the correction box above:
|
||||
822.7 ms → 51 477.4 ms on re-measurement, standalone, same unit.**
|
||||
|
||||
What this does and does not license:
|
||||
|
||||
- It does **not** license changing either pattern here. Both are byte-identical to
|
||||
`llm-security`'s source, and the behaviour-preservation invariant is not suspended because
|
||||
the measurement is unwelcome. Commons carries `[^>]` on provenance and would carry `[^><]`
|
||||
had the source been the guard's — that was true before this measurement and stays true after.
|
||||
- It does mean the finding is **owed to the owning runtime**, which is `llm-security` for both
|
||||
patterns. Sent 2026-08-11, with these numbers and the `iframe-src` row they did not have.
|
||||
|
||||
**Outcome, same day:** the owning runtime reproduced both rows on its own vendored copy,
|
||||
measured the exposure at its real entry-point caps (which the numbers above deliberately do not
|
||||
cover — a pattern cost is not an exposure), judged it unacceptable, chose `[^><]*` over
|
||||
`[^>]{0,256}` on the grounds that a bound is paddable, and asked commons to carry the change.
|
||||
Commons `0.8.0` carries it. Note what did **and did not** license that: the measurement above
|
||||
still licenses nothing on its own, and the two bullets stand as written. What licensed the
|
||||
change is the *owner's decision*, arriving through the one channel this repository accepts for
|
||||
moving a `pattern` value. Had they declined, the rows would still read `[^>]*` today and this
|
||||
document would record a permanent divergence instead.
|
||||
- The guard's own honest disclosure belongs on the record too: their suite passes under both
|
||||
forms (717 green), because the DoS row that ought to discriminate uses `'<script>'` as its
|
||||
repeating unit, which supplies the `>` and never exercises the run. The invariant is
|
||||
documented there and not test-enforced. A conformance case could hold it — but a fixture that
|
||||
pins a *timing* property is a different kind of case from anything in this corpus today, and
|
||||
minting one is not commons' call to take alone.
|
||||
|
||||
## Severity: the 8 hybrid patterns
|
||||
|
||||
**Resolved 2026-08-09. The two sides never disagreed; only the evidence did.**
|
||||
|
|
|
|||
|
|
@ -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)",
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"version": "0.1.0",
|
||||
"version": "0.3.0",
|
||||
"id": "secret-egress",
|
||||
"description": "Credential and token shapes that must never leave a machine: the fixed pattern table a pre-write guard matches against content before it is persisted. Detection data only - what to DO when one matches (block, warn, redact) is the consumer's policy and is not described here.",
|
||||
"owasp": "LLM02",
|
||||
"match_semantics": "first match wins; patterns are evaluated in ascending `order`",
|
||||
"$comment": "Extracted without behaviour change from llm-security/hooks/scripts/pre-edit-secrets.mjs (`SECRET_PATTERNS`), delivered as operator dump 2/2 through the local coord mailbox on 2026-08-09. NOTE THE SOURCE FILE: the dump states explicitly that this is the engine-consumed hook table and NOT knowledge/secrets-patterns.md, which is a separate PCRE-flavoured agent-consumed variant that stays where it is. This repository's own extraction plan originally named the wrong one of the two; the file recorded here is the one that was actually delivered. Only the 18 fixed entries are data - the dump states that entries 19 and beyond are policy-injected custom patterns at runtime and are not part of the base table.",
|
||||
"$comment": "Extracted without behaviour change from llm-security/hooks/scripts/pre-edit-secrets.mjs (`SECRET_PATTERNS`). NOTE THE SOURCE FILE: this is the engine-consumed hook table and NOT knowledge/secrets-patterns.md, which is a separate PCRE-flavoured agent-consumed variant that stays where it is. This repository's own extraction plan originally named the wrong one of the two; the file recorded here is the one that was actually delivered. Only the 19 fixed entries are data - entries 20 and beyond are policy-injected custom patterns at runtime and are not part of the base table. Version 0.3.0 re-extracted the table from a pinned public commit rather than from a transcription: positions 0-16 were confirmed byte-identical to what the 0.1.0 dump had already produced, and order 17 (`OpenAI Legacy API Key`) was read out of the module at that commit. The one-entry staleness disclosed in 0.2.0 is therefore closed by measurement, not by transcribing the regex out of the coord message that reported it.",
|
||||
"provenance": {
|
||||
"source_repo": "llm-security",
|
||||
"source_files": [
|
||||
|
|
@ -13,13 +13,15 @@
|
|||
"source_exports": [
|
||||
"SECRET_PATTERNS"
|
||||
],
|
||||
"source_delivery": "operator dump 2/2, coord message from llm-security, 2026-08-09",
|
||||
"source_commit": "unknown - not supplied with the dump",
|
||||
"verified": "differentially, against the dump",
|
||||
"source_delivery": "0.1.0: operator dump 2/2, coord message from llm-security, 2026-08-09. 0.3.0: re-extracted from the module text at a pinned public commit, obtained with `git show <commit>:hooks/scripts/pre-edit-secrets.mjs` against the public remote.",
|
||||
"source_commit": "47905dacae8bd5613c8ed76c088cb3de93f40091",
|
||||
"source_commit_note": "`refs/heads/main` on the public remote when 0.3.0 was extracted. The commit that introduced order 17 is `088e458`, confirmed here to be an ancestor of the pinned head with `git merge-base --is-ancestor`, rather than accepted from the reporting message.",
|
||||
"verified": "positionally against the module at the pinned commit: name, regex source, flags and order compared for all 19 entries, 0 divergences",
|
||||
"evidence_limits": [
|
||||
"The dump is a transcription of the source module, not the module file itself. The checks recorded for this file prove that this JSON agrees with the DUMP; dump-to-module fidelity is llm-security's assertion, not a result reproduced here.",
|
||||
"No severity, and no per-entry disposition, was supplied. The source table carries a name and a pattern and nothing else, so neither is invented here.",
|
||||
"The runtime-injected custom patterns (entries 19+) are policy, not data, and are out of scope. A consumer that matches only this table matches LESS than the seed hook does when a policy is loaded."
|
||||
"The runtime-injected custom patterns (entries 20+) are policy, not data, and are out of scope. A consumer that matches only this table matches LESS than the seed hook does when a policy is loaded.",
|
||||
"Fidelity is asserted against ONE commit. The pinned commit above is where this table was read; it says nothing about any later state of the source module. A consumer that needs to know whether it has drifted must re-measure against the remote, not re-read this field.",
|
||||
"The comparison covers what the table declares - name, pattern source, flags, order. It does not cover the surrounding module: how the guard reads content, what it does on a match, or which paths it exempts are engine behaviour and are deliberately absent from this file."
|
||||
]
|
||||
},
|
||||
"ordering": {
|
||||
|
|
@ -29,7 +31,7 @@
|
|||
},
|
||||
"dialect": {
|
||||
"name": "ecmascript",
|
||||
"$comment": "Patterns are ECMAScript regular-expression source text exactly as the source literals spell it. Flags are declared per pattern; an entry with no `flags` key carries no flags. All 18 compile in Node with their declared flags, in Node with `u` added, and in Python `re` with the equivalent re.I.",
|
||||
"$comment": "Patterns are ECMAScript regular-expression source text exactly as the source literals spell it. Flags are declared per pattern; an entry with no `flags` key carries no flags. All 19 compile in Node with their declared flags, in Node with `u` added, and in Python `re` with the equivalent re.I.",
|
||||
"flags": {
|
||||
"i": "case-insensitive"
|
||||
},
|
||||
|
|
@ -40,13 +42,13 @@
|
|||
"character classes"
|
||||
],
|
||||
"translation_notes": [
|
||||
"Python (`re`): compile with re.I where flags contain `i`. No rewriting needed; verified by compiling all 18.",
|
||||
"Python (`re`): compile with re.I where flags contain `i`. No rewriting needed; verified by compiling all 19.",
|
||||
"Two patterns contain `\\/` - the redundant escape a JavaScript regex LITERAL requires and that `RegExp.prototype.source` preserves ('Slack/Discord Webhook URL' and 'Database connection string'). Kept byte-identical because Node bare, Node under `u` and Python `re` all accept it. Engines that reject unknown escapes (Go `regexp`, RE2) MUST report these two as unsupported rather than skip them silently.",
|
||||
"The 'Generic credential assignment' and 'Authorization header with token' entries are shape matches, not proofs of a live credential. A consumer treating every match as a confirmed leak will produce false positives; that trade-off belongs to the consumer's policy, not to this table."
|
||||
]
|
||||
},
|
||||
"normalisations": [],
|
||||
"normalisations_note": "Empty by result, not by omission: all 18 patterns are byte-identical to the source, verified below. No escaping change was needed.",
|
||||
"normalisations_note": "Empty by result, not by omission: all 19 patterns are byte-identical to the source module at the pinned commit, compared positionally. No escaping change was needed.",
|
||||
"patterns": [
|
||||
{
|
||||
"order": 0,
|
||||
|
|
@ -141,9 +143,14 @@
|
|||
},
|
||||
{
|
||||
"order": 17,
|
||||
"name": "OpenAI Legacy API Key",
|
||||
"pattern": "\\bsk-[A-Za-z0-9]{20}T3BlbkFJ[A-Za-z0-9]{20}\\b"
|
||||
},
|
||||
{
|
||||
"order": 18,
|
||||
"name": "JWT (three-part token)",
|
||||
"pattern": "\\beyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\b"
|
||||
}
|
||||
],
|
||||
"count": 18
|
||||
"count": 19
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue