Affected: 0.7.0 and earlier. Fixed: 0.8.0. Severity: high for any host that persisted or transported approval objects.
In 0.7.0 an Approval was bound only to the canonical action digest. A digest
identifies an action class, not one execution of it, and the gate kept no
record of which grants had been spent. The same approval object therefore
re-authorised every later action with identical parameters, without limit. We
confirmed this against the published 0.7.0 code: three consecutive decide()
calls with one approval returned allow / EXACT_ACTION_APPROVED each time. The
--approval CLI path was the practical exposure, since an approval file could
be replayed indefinitely; the HTTP endpoint was never affected, because it has
always refused caller-supplied approvals.
0.8.0 binds an approval to one action instance. mobius.action-approval.v2
adds a nonce, an audience, and a not_after expiry, and promotion now also
requires a consumption ledger that records the spent nonce. Every missing host
input fails closed with its own reason code — APPROVAL_LEDGER_UNAVAILABLE,
APPROVAL_AUDIENCE_UNAVAILABLE, APPROVAL_AUDIENCE_MISMATCH,
APPROVAL_EXPIRED, APPROVAL_ALREADY_CONSUMED — and a grant refused for any
reason other than consumption stays unspent. v1 approvals are rejected outright
rather than accepted without a nonce.
Action required: upgrade to 0.8.0, reissue any stored approvals in the v2
schema, and supply a durable ledger. FileApprovalLedger is a local-operator
convenience; a deployment that must not lose the record needs independently
administered, append-only retention, because a lost ledger silently restores
replay.
Affected: 0.8.0 and 0.8.1. Fixed: 0.8.2. Severity: high for hosts
using FileApprovalLedger; the approval-surface defects are high wherever a
human reads the summary to decide.
Adversarial review of our own fix found four defects in it. We publish them in full because two of them made claims in our own documentation false.
- The file ledger was not atomic.
FileApprovalLedgerread the ledger and then appended, with no lock between. Concurrent callers each saw a ledger without the nonce and each concluded the grant was unspent. Measured: twelve concurrent processes presenting one nonce, eight received permission. Agents issue tool calls in parallel and the CLI runs one process per call, so this is the normal case. This is the replay of MG-2026-001 reached through a different door. Fixed with an exclusive lock (flock) spanning check and append;InMemoryApprovalLedgeris likewise now atomic. - The approval summary was not injective, contrary to our claim. Only
values were JSON-encoded;
operation,tool, argument keys, andsource_idswere interpolated raw. Two requests with different digests could render identically, and a single untrusted source could be made to read as a trusted operator plus an untrusted page. Fixed by encoding every interpolated element and rendering sources one per line with a count. - Compound disclosure never fired on the adapter path. The scan covered
only top-level string arguments, but the
PreToolUseadapter puts the command intarget. Chained commands reached approvers with no warning. Fixed by scanningtargetand recursing into nested lists and mappings, by adding<(,>(, and$'to the token set, and by correcting the adapter: it truncated the command at 10,000 characters and preferredfile_pathovercommand, either of which pushed the chaining operators out of view. The adapter now forwards the markers it finds in the fields whose value is executed (command,cmd,script,argv,args,shell), at any nesting depth. Two exclusions are deliberate and therefore undisclosed by design: a bare&, because it appears in ordinary query strings; and command-bearing fields under other names, because scanning every field made the warning fire on most ordinary calls — file content, edit replacements, and search patterns legitimately contain newlines and pipes. A warning that fires constantly is trained away, which is the same failure as not warning. A tool that passes a command under an unrecognised key is not disclosed as compound; such tools are outside the declared set and reachaskasunknown. - Abbreviated values carried a 64-bit hash prefix while the documentation said "full SHA-256" and "cannot". Fixed by emitting the full digest.
Also corrected: the HTTP app advertised version="0.7.0" — the version
withdrawn under MG-2026-001 — through /openapi.json; and not_after had no
ceiling, so a self-asserted expiry could be perpetual (now bounded, default 24
hours).
The single-use ledger defends against replay of a captured approval. It does
not defend against forgery of a new approval. Authenticity rests entirely on
the host-side approval_verifier, and the verifier shipped in this repository
is a stub that returns true (cli.py): it marks the local-operator boundary,
it does not authenticate anything. Anyone who can write an approval file can
mint fresh nonces without limit. A deployment that needs approvals to be
unforgeable must supply a verifier that checks a signature over the whole
approval, including the nonce.
The ledger also cannot defend its own storage: deleting or truncating it
restores replay, and the governed agent has Bash, which is gated at ask
rather than denied. Independently administered, append-only retention is not a
nicety here.
Version 0.8.x is an alpha implementation. Security fixes may change policy behavior; pin exact versions and rerun your own regression set before updating.
Report privately through GitHub's private vulnerability reporting, which is enabled on this repository: open the Security tab and choose Report a vulnerability. Do not include credentials, private prompts, customer data, or live exploit payloads in a public issue.
We publish defects in our own security machinery, including defects in our own fixes — MG-2026-002 is an advisory about MG-2026-001's remedy. Expect the same treatment for anything reported here.
What this project can promise. Advisories are published in SECURITY.md,
in the changelog, and in GitHub Releases, and superseded releases are annotated
in place. There is no individual notification channel: this is an unfunded
alpha with no subscriber list, and neither the AGPL grant nor a commercial
licence adds a duty to contact anyone. Watch releases if you need to know.
This component does not claim complete prompt-injection prevention. It assumes the caller still enforces least privilege, explicit approval for consequential actions, sandboxing, audit logs, and rollback. Scanner admission is never authorization for a tool action.