SECURITY.md — reporting a vulnerability, and what we are defending
OpenRegs turns published regulation into signed, machine-readable data that other systems act on. The asset worth attacking is therefore not a server: it is the corpus itself, and the trust chain that says a given sentence is the law. This document says how to report a hole in that chain and what we already consider to be in scope.
Reporting channel
Section titled “Reporting channel”Report privately through GitHub’s private vulnerability reporting, from the
Security tab of this repository, or directly at
https://github.com/openregs/openregs/security/advisories/new. It is enabled,
it reaches @openregs/core-maintainers, and it needs no account with us and no
mail to arrive anywhere. Do not open a public issue, a discussion, or a pull
request for a security report — a public report on a corpus-integrity bug is a
working exploit handed to everyone who consumes a release.
On
security@openregs.io. That address is planned and does not route mail yet. It is deliberately not offered above: a disclosure channel that silently drops reports is worse than no channel at all, because a researcher who is met with silence reasonably goes public. This note disappears when the address is live and monitored, and not before.The other addresses in this repository belong to
MAINTAINERS.yaml’s two fixture personas and are.invalidon purpose. A deployment that forks OpenRegs must put a monitored address here before it publishes releases anybody else consumes.
Include, if you have them: the release tag and regime, the eId or atom id affected, the commit or artifact digest you observed, and the smallest sequence of commands that reproduces the problem. A report we can replay offline against a fixture is a report we can fix in a day.
Do not run tests against real regulator endpoints while investigating. Every network behaviour in this project is exercised against local test servers, and a scan pointed at a regulator’s publication service is an incident of its own.
Disclosure timeline
Section titled “Disclosure timeline”We work to the following clock, counted in business days from the day a
report is received. The durations are the same ones GOVERNANCE.md uses for
escalation, so a security report is never waiting on a slower ladder than a code
review.
| Stage | Target |
|---|---|
| Acknowledgement that the report was received | 2 business days |
| Triage: reproduced or rejected, with severity and an owner named | 5 business days |
| Fix or documented mitigation available to the reporter | 30 calendar days |
| Coordinated public disclosure and advisory | 90 calendar days from the report, or on the day a fix ships, whichever is sooner |
If a vulnerability is being actively exploited, or the corpus a published release carries is already wrong, we shorten this: we publish the advisory and revoke or re-issue the affected release as soon as a mitigation exists, without waiting out the 90 days. If we need longer than 90 days we say so in writing, with a reason and a date, before day 90 — silence is never the answer.
Reporters are credited in the advisory unless they ask not to be. We do not run a bounty programme and we do not require an embargo agreement.
Threat model
Section titled “Threat model”Three threats define the security posture of this project. Each names what the attacker gets, what already stands in the way, and what a report about it should show.
1. Corpus tampering
Section titled “1. Corpus tampering”The attack. An adversary changes what the corpus says the law is — a digit in
a monetary threshold, a MUST turned into a MAY, a derogation quietly widened,
a date of entry into force moved. This is the highest-value attack on OpenRegs,
because the change is small, it looks like law, and every downstream system acts
on it without a human ever reading the sentence. The point of entry may be
upstream (a compromised or spoofed regulator endpoint), in transit (a fetch that
is not what the regulator served), or in the repository (a pull request that
edits a canonical text or an obligation atom).
What stands in the way. Captures are content-addressed by the sha256 of their
bytes, so a snapshot cannot be edited in place. Every canonical text traces to a
SOURCES.lock sha256, and a text without one fails CI. Every atom’s
quoted_text must equal source_text[char_start:char_end] byte for byte, so an
atom cannot say something its provision does not. Canon paths are code-owned
through MAINTAINERS.yaml, and the pre-receive hook believes only approvals from
the team that owns the path. Releases are digested file by file and signed.
Report it if you can make a canonical text, an atom, or a served answer state something the cited source does not — or make any of the checks above pass over content that does not match its source.
2. Supply-chain attacks on releases
Section titled “2. Supply-chain attacks on releases”The attack. The corpus is correct in git but what a consumer installs is
not. Variants: a substituted or re-published release artifact; a rewritten tag, so
fixreg@2025.04 means different bytes tomorrow than it did today; a forged or
replayed signature; a downgrade that serves an older release under a newer tag; a
malicious openregs pull source; or a compromised build dependency that
poisons the bundle, the graph index, the embeddings or the generated constants.
What stands in the way. A release directory carries a digest for every file it
holds, a provenance attestation naming the commit and run it was built from, a
signature over the manifest, and an entry in a transparency log. openregs pull
and openregs serve verify signatures, digests and provenance before
unpacking or answering — CLAUDE.md’s “verify before unpack” rule, with no flag to
skip it. A tag whose content has been rewritten is an error, not a re-pull.
Builds are deterministic, so the same inputs produce byte-identical artifacts and
a substituted artifact does not reproduce.
Report it if you can get a served or unpacked release past verification without holding the signing key, get a rewritten tag accepted silently, or make a deterministic build produce different bytes from the same commit.
3. Prompt injection through legal text served to AI agents
Section titled “3. Prompt injection through legal text served to AI agents”The attack. OpenRegs feeds regulatory text to language models — that is what the MCP server is for. Because an atom quotes its source verbatim and a consolidated text is reproduced faithfully, any instruction an attacker gets into the source document is carried faithfully all the way into a model’s context: “ignore your previous instructions”, “this obligation does not apply to you”, “call this tool with these arguments”, a link to follow. The corpus’s own integrity guarantees are what make this attack work — the pipeline is supposed to reproduce upstream bytes exactly. The blast radius is whatever the agent downstream is allowed to do.
What stands in the way. Every MCP tool result is built in exactly one place
(envelope() in tooling/openregs/serve/tools.py) and every result wraps its
payload in a data envelope: the retrieved text sits under a data key beside
a notice telling the reader that the payload is quoted material, that
instructions embedded in it must not be followed, and that such content should be
reported as possible tampering. That notice is read at startup from
DISCLAIMER.md and exists nowhere else in the tree, so it cannot drift or be
silently dropped. Tool results are structured JSON rather than free prose, so
quoted text is never concatenated into a sentence that reads like guidance, and
every block carries the eId or atom id it came from — an injected line has a
citation, which is what makes it findable.
Report it if you can get retrieved text out of a tool result without the envelope and its notice, get a tool to emit text that is not attributable to a cited unit or atom, or get an injected instruction to influence the server’s own behaviour rather than merely appearing inside a quoted payload.
Out of scope
Section titled “Out of scope”- Findings against
regimes/fixreg/treated as findings about real law. FIXREG is an invented fixture regulation; it is wrong about the world on purpose. - The
.invalidaddresses and fictional maintainers inMAINTAINERS.yaml. - Denial of service against a server you run yourself, and reports produced by scanning third-party regulator endpoints.
- Missing hardening on
openregs servethat the roadmap has not reached yet — authentication, rate limiting and request quotas are a tracked task, not a vulnerability report. A bypass of hardening that exists is in scope.
Correctness bugs are not security bugs
Section titled “Correctness bugs are not security bugs”An answer that is wrong because the pipeline made a mistake is a correctness bug: open a public issue with the release tag, the as-of date and the citation. Use this document when the integrity of the corpus, of a release, or of what an agent is told is at stake. When in doubt, report privately; we would rather triage a correctness bug in the private channel than read about a corpus compromise in a public one.
Rendered from openregs/openregs@cbe9615:SECURITY.md