A worked example. This is the artifact produced by an Adversarial Issues Register engagement, run here against our own code so it can be published in full. On a client engagement the findings would be yours and this document would be private; the format, the severity discipline and the treatment of what stays unfixed are the same.
Subject: magus-opensecmcp — a local execution firewall for MCP agents (Rust).
Reviews: three independent adversarial passes, February–August 2026.
Status: current as of commit 235b9f3.
What a register is for
Most security reviews produce a list of things to fix. That is the easy half. The harder and more useful half is a defensible record of what remains unresolved, how severe it is, and whether a solution path exists — because that is what you need when someone asks you to justify shipping.
A register is therefore written to be read by three people: the engineer who has to fix it, the person deciding whether to ship, and whoever asks about it in six months.
Method
Four rules, applied to every finding.
Reproduce, do not reason. A finding is not accepted until it has been demonstrated against the running system. Every entry below is labelled accordingly, and reasoned-only findings are marked as such rather than presented as confirmed. Three findings in these reviews were reported and then downgraded or reframed when reproduction did not support the reported version.
Ask what a test would catch, not whether it passes. Five defects in this codebase had their load-bearing property protected only by tests that would have passed for other reasons — decay tests fed literals the classifier could never produce; a corpus that asserted control tokens fire and never that benign files don't; a collision test exercising the boundary where the fix already held. Every one had a green suite.
Fix the layer the problem lives in. A calibration number cannot fix a measurement primitive's blind spot; a policy concept does not belong in an observation module. Several findings here were initially scoped to the wrong layer and re-scoped before work started.
A guard is not a guard until you have watched it fail. Introduce the violation the guard exists to catch and confirm it catches it.
Register
Severity reflects real-world impact under realistic deployment, not theoretical maximum.
| ID | Severity | Finding | Status |
|---|---|---|---|
| C1 | Critical | Provenance state machine permanently blocked all High-risk actions after the first real response | Closed |
| R3-1 | High | Session risk budget bricked legitimate sessions after ~46 calls, un-tunable | Closed |
| H2 | High | Model-control-token rules poisoned sessions on ordinary source code and model configs | Closed |
| H3 | High | One Unicode confusable defeated the entire signature layer | Closed |
| F1 | High | Provenance decay driven by an agent-controlled value ("decay bombing") | Closed |
| F2 | High | Regex rules evaluated only the first match per response | Closed |
| M4 | Medium | Hash-pin enforcement — the headline defence — defaulted to off | Closed |
| F3 | Medium | Multi-server tool-name collisions resolved silently by config order | Closed |
| F4 | Medium | Trusted-server tool descriptions were scanned but never sanitised | Closed |
| F5 | Medium | No timeout on downstream RPC reads; one stalled server wedged the gateway | Closed |
| R3-2 | Medium-High | Description-injection defence inert against plain language; inputSchema never scanned | Open |
| R3-3 | Medium | Response scanner never inspects JSON object keys | Open |
| R3-4 | Medium | Detection mechanically evadable, so the capability gate rarely engages | Open |
| R3-5 | Medium | A documented deterministic guarantee is unreachable at runtime | Open |
| L7 | Low | Advisory delivery outcome computed and discarded; failures unrecorded | Closed |
| L8 | Low | Hasher violated its own stated collision-resistance discipline | Closed |
| F7 | Low | No state persistence across restarts | Open, deferred |
| — | Low | Replay protection unreachable by construction; its only live effect was a session brick | Closed |
| — | Low | Rejection audit records log declared rather than effective risk class | Open |
Selected entries, worked
Four chosen to show the range: a critical one, one that was reported wrongly, one found while investigating something else, and one that stays open.
C1 — the gate that never opened (Critical, closed)
Reproduced. Against a normally-behaving, never-compromised server, every High-risk action was refused permanently after the first tool call.
compute_new_state structurally could not return Clean for any response shape a real
MCP server produces. Once a session reached Elevated — which happened on essentially the
first call — a High-risk action was escalated to Critical, and Critical requires
Clean. Decay could not recover it either: it could only earn progress from responses
that were themselves Clean, and no real response could ever be.
Why the tests passed anyway. Every decay test hand-fed the tracker a Clean literal
the classifier could never actually produce, and none chained through both tracker methods
the way production does. The mechanism was correct in isolation and unreachable in place.
Layer. Three coordinated changes, none of them a threshold tweak: the classifier stopped
treating response shape as an anomaly, decay progress was gated on each tier's own target,
and the Elevated → Critical bump was removed.
R3-2 — reported one way, worse in another (Medium-High, open)
The reported finding was that description scanning missed natural-language injections. Reproduction confirmed it and found the mechanism is structural: every rule capable of recognising an instruction is scoped to tool output, so on a tool description only four control-token rules can fire at all. A plainly-worded injection matches nothing.
It also refuted our own prior conclusion. An earlier register entry had reasoned that the capability was "intact three other ways," one of which was a strict-scanning mode. Reproduction showed strict mode escalates a hit — and a plain-language description produces no hit to escalate. The mitigation we had recorded does not exist.
Separately, inputSchema is forwarded to the agent verbatim and never scanned, which is a
documented MCP attack vector left fully open.
Still open. Fixing it requires a scoping decision with a real false-positive cost, not a patch.
Replay protection — unreachable by construction (Low, closed)
Found while investigating an unrelated availability defect.
Proposal identifiers are minted by the gateway itself. They are never supplied by the agent, the downstream server, or the request. A duplicate is therefore impossible and the replay check could never fire. Its only live effect was a 100,000-entry cache that eventually refused every call, reads included.
Its tests passed because they used fixed string identifiers and inserted duplicates by hand — exercised with input production cannot generate. Before removing the rejection codes we grepped 11,297 real audit records to confirm neither had ever been emitted, rather than asserting it.
This is the third mechanism found unreachable in production. That pattern is now a tracked work item in its own right.
R3-4 — the honest one (Medium, open)
The threat model's central claim is that detection is a signal layer and the capability gate is the thing to trust. Reproduction showed the gate is downstream of detection: a session only becomes gated if a rule fires, and a single comma inserted into a canonical injection phrase defeats the rule that would have fired.
We had shipped a fix, weeks earlier, closing character substitution evasion. Character insertion defeats the same rule. The earlier claim was true and narrower than it read.
Still open, and possibly permanently. No finite signature list closes paraphrase. The honest resolution may be a documentation change rather than a code change.
What is still open
Stated plainly, because a register that only lists wins is marketing.
- Description and schema injection (R3-2). Plain-language injections in tool descriptions
are not detected;
inputSchemais not scanned at all. - Object keys are never scanned (R3-3). An entire syntactic position the model reads is invisible to the detection layer.
- Detection is mechanically evadable (R3-4). And the capability gate is downstream of it.
- A documented deterministic guarantee is unreachable (R3-5). One of two "no false-positive class" poison triggers cannot fire in the live pipeline.
- No state persistence across restarts (F7). A
Poisonedverdict does not survive a client restart. - Rejection audit records mislabel effective risk. The field exists to record the post-modulation value and records the declared one, on every rejection.
What this register does not license you to conclude
That the closed items are closed correctly in your environment. Every fix here was verified against this codebase, driven end-to-end against a real downstream server. That is evidence about this system, not a general guarantee.
That the open items are the only open items. A register records what was found by the passes that were run. The correct reading of "four findings in one week, three found by accident" is that the count is found, not exists.
That severity is transferable. These ratings assume this deployment. The same defect in a system with different blast radius is a different severity.
Produced by VaHive Systems Lab. If you want one of these for a system you actually run, the engagement is described under Services.