Hack Hydra 2026 · Track 01 · built on HydraDB
A fact inferred from three documents is not a document. It has no ACL of its own — so document-level filtering, which is what every enterprise AI assistant does, has no answer for it. We measured the hole and closed it — then attacked our own proof and published what that cost.
“What changes did the Engineering Lead suggest for the AnomalyForce product vision?”
00 · the turn
We closed the hole and proved it sound. Then we asked the question a security reviewer asks next — when Cordon refuses, does the asker end up not knowing? — and found that our own theorem does not answer it.
The proof is about provenance: what the system hands over. A
reviewer cares about content: what you end up knowing. Our
derivation rules ship in this repository under Apache 2.0, so an attacker
does not reverse-engineer them — they git clone
them. We ran our own rules over the facts we had disclosed.
| 120,206 denials, re-derived by our own published rules | Count | Share |
|---|---|---|
| Phantom — rebuilt from what we did disclose | 1,208 | 1.0% |
| Effective — genuinely withheld | 118,998 | 99.0% |
A refusal the asker can undo is not a refusal. It costs them an answer, costs the operator a ticket, and returns nothing — while appearing on a dashboard as protection. It is a lie the system tells its owner.
Depth 1 is tight — 0 phantoms in 96,206 denials. Depths 2 and 3 are not, and the cause is a fix we were right to make: we raised a requirement after traversal caught it understated. Correct for provenance, worth nothing for content. Closing that gap is a minimum cut, not a stronger requirement — and it costs 37.7% of an asker’s legitimate evidence.
Which would be unshippable — unless you stop deciding one fact at a time. Deciding over the set, so the disclosed answer’s closure cannot rebuild anything the asker was refused:
| Retrieval depth | Plans constrained | Evidence retained |
|---|---|---|
| top-10 | 0 | 100.0% |
| top-20 — production depth | 0 of 1,200 | 100.0% — free |
| top-50 | 12 | 97.6% |
| top-200 | 192 | 88.8% |
Inference safety is free at production retrieval depth, and first bites at k=50. A measured phase transition, not a claim. And because ten individually safe answers can jointly leak, a per-principal disclosure ledger carries the guarantee across a session: 100% retained at query 1, 80.7% by query 30. Confidentiality stops being yes-or-no and becomes a budget.
Over 150 single grants: 713,882 documents disclosed — and 380 derived facts, of which 100% were unlocked in combination with access the person already held. Nobody approved those. They are not documents, so no access review shows them.
Every leak number says “lower bound, because our adversary runs our rules.” So we pointed a language model at the denials we had called protected. It recovered nothing — and we can say why: 0 of 16,594 documents name a product area other than their own. That measures the corpus, not our defence.
A refusal record carrying the withheld text is a second copy of the secret in a file more people can read. Cordon’s log stores identifiers only and refuses at write time to store content. It is hash-chained, so an edited or deleted entry is detectable — verified against four kinds of forgery.
Provenance is not content · Inference-safe planning · What a grant costs · Testing our own lower bound
01 · the measurement
1,514 questions from Salesforce’s HERB enterprise benchmark, 12 principals across the access spectrum, 18,168 trials per system. Identical retrieval, identical answer assembly. The only difference is what each is willing to disclose.
| Measured over 18,168 trials | Document-level ACL | Cordon |
|---|---|---|
| Leak rate | 17.4% | 0.0% |
| Answer quality (set-F1) | 0.099 | 0.099 |
| False denials | 0 | 0 |
| Disclosure decided by ingest order | 1,008 of 7,280 | never |
| Verified against | its own model of access | GitHub’s own 404 |
Zero leaked at derivation depth 0. That is not a compliment we are paying the baseline — it is the finding. Document filtering does not have a bug. It has a ceiling, and the ceiling is the first inference.
Facts leaked by document-level filtering, by derivation depth. Cordon: zero on every rung.
02 · worse than wrong
A derived fact carries one space, assigned by whichever source the writer reached first. That assignment is what a document gate reads. So we asked: would it have answered differently had the node been filed under a different one of its own sources?
Same graph, same permissions, same person asking — opposite answer. A security decision settled by ingest order is not a security decision. docs/RESULTS.md · regenerate with npm run audit
03 · not our opinion
The strongest objection to any of this is that we invented the access control we then enforced. So the whole pipeline runs a second time against a system that already has permissions: 8 real GitHub repositories — 5 private, 3 public — inside a real organisation with 11 nested teams as principals. Only the loader differs. Extraction, resolution, derivation and the admissibility rule are the same code.
For every source underneath a withheld fact, the audit issues an unauthenticated request and asserts the refusal:
The check deliberately uses github.com, not the API.
The unauthenticated API allows 60 requests an hour and then answers 403
to everything — and counting a rate-limit 403 as a pass would fabricate the exact
result this section exists to establish. Rate limiting is detected and reported separately
from pass and fail.
04 · the attack nobody else can express
Two facts a principal is entitled to can together determine a third they are not. Document-level filtering cannot defend against this even in principle, because the thing being aggregated is not a document. We formalised it, proved what is impossible, and mined the rest from the graph rather than inventing examples.
An attacker cannot climb the derivation edges. required(f)
is the union of its supports’ requirements, so holding every part of a conclusion
already entitles you to the conclusion.
0 counterexamples over every case in the graph.
Cordon is closed under aggregation exactly when every claim about a space is only asserted by facts resting on that space — claim locality. So the exposure is a property of the corpus, not of the rule.
HERB: 0 of 38,600 artifacts name another space. Locality holds exactly.
So we seeded a fixture with the cross-repository references real issues are full of — and Cordon leaked 16 times out of 130 denied pairs. We are reporting that because it locates the mechanism exactly: a public issue naming a private repository.
“Ingrid Holm notes the SDK release depends on cordon-demo-borealis integration work.” public repository · asserting a claim about a private one
The fix follows from the diagnosis: widen the requirement to include the spaces a fact names, not only where its evidence sits. Leaks go to zero, and it costs 54 additional withholdings. Both rules ship with their numbers; the operator picks.
Measured and priced. 1,208 of 120,206 denials are rebuilt by our own published rules. Closing them is a minimum cut costing 37.7% of readable evidence globally — and 0% per answer at production retrieval depth.
Our own defence opens it. Naming the space you lack is useful to a colleague and a perfect oracle to an attacker. Measured in bits, with an indistinguishable-abstention mode that closes it and the cost stated.
Whether you perceive a contradiction at all depends on your clearance. Two colleagues ask the same question and receive confidently opposed answers, and neither is told the other side exists.
05 · why this needs HydraDB
The requirement is discovered by traversal, per asker, at query time — never read from a field. It cannot be materialised: with n principals there are 2n visibility subsets, and precomputing per principal is 30M rows that one membership change invalidates.
A level-3 fact sits four hops from any evidence and has no edge to a space at all. There is no field to read. The answer only exists as a walk.
And a vector index cannot express this even in principle. An embedding records what a fact resembles; it carries no record of what it was derived from, and a constraint that propagates along derivation has nothing to travel down. Similarity is not provenance.
Building this, we probed what the OpenCypher subset actually supports and found queries silently truncating at 1,024 rows with no error — which for a security system means a quarter of an authorization table disappearing quietly. Our client fails closed on it. All three findings are filed upstream with reproductions: #115, #116, #117. We also publish what does not run in the engine: the transitive org closure is application-side, because variable-length traversal requires a fixed source id. A reviewer who finds that themselves reads it as a hole; measured and published, it is a capability map.
06 · how to disbelieve us
Not one figure here is typed into a markdown file. The audit emits every trial as a committed row; the test suite recomputes each published score from those rows and fails if the README, the summary and the raw data disagree.
“Identical retrieval, only disclosure differs” used to be a comment in the source. Each of the three arms now hashes the candidate list it was handed, computed from its own view rather than a shared variable, and the audit exits non-zero if they ever disagree. A comparison between arms that saw different evidence is not a result, so it should not be publishable.
Cordon’s whole thesis is auditability, so nothing here is estimated, projected or illustrative. Raw artifacts carrying the git SHA and timestamp are committed alongside the code.
The invariant compares requirements obtained by graph traversal against requirements recomputed independently from the corpus. That distinction is not pedantry: our first version compared a value against itself, could not have failed, and passed for hours. When it was fixed, 47 facts disagreed — and that is how a real access-control bug was found.
A security property only ever checked against the field that produced it is not being checked at all. the thing we would most want a reviewer to take away