Attack Lab #5: We Paid One Authorization Twice — and Pushed the Audit Fingerprint Beyond Our Own Reach
Two breaks at once. One: with no single-use nonce, a fully-valid payment could be replayed until the budget drained — we added an anti-replay gate (replay → 409). Two: Episode 4's hash chain lived only in our own DB, so an admin rewriting the whole tail couldn't be disproven — we exposed the tip publicly (/api/audit-anchor) and had GitHub witness it on a schedule. Honest limit: true non-repudiation still needs anchoring to a public chain.
Attack Lab · Episode 5. Last episode we admitted a hole we hadn't closed: the hash chain catches deletions and edits, but if an admin rewrites the entire tail, the only place that could prove what the original fingerprint was — was still our own database. This episode closes it. And along the way we broke one more thing: replay.
Attack 1: one signed authorization, paid twice
The gateway has eight gates; a legitimate payment passes them in turn: attribution, proof, per-tx limit, daily velocity, allowlist, intent match, AML… all pass, settle. Looks airtight.
The problem: it never checked whether this was the second time it had seen the same request.
Real run, live gateway, before the fix:
1) An agent sends a legit payment (0.05 USDC, allowlisted payee, correct purpose)
→ 200 allow, settled
2) Resend that exact request, unchanged
→ 200 allow, settled AGAIN
3) Loop N times (as long as you stay under the daily cap)
→ one authorization, paid N times
Every gate says "pass" — because it is a legitimate request. The daily cap is the only backstop, but under that cap a single authorization can be replayed until the budget is drained.
Real-world analogy: an unnumbered check — photocopy it, cash each copy. Or a card charge with no idempotency key: the network hiccups, you get billed twice.
The patch: a single-use nonce (anti-replay / anti-double-spend)
The fix is tiny: every payment must carry a unique, one-time nonce, consumed once before settlement.
- First time we see the nonce → record it, allow, settle.
- Same nonce again → that's a replay, blocked at
Mandate/Replay(409). - Two identical requests arriving at once? The database's unique constraint guarantees only one writes; the other is rejected on the spot — the race is closed too.
We deliberately put this gate last, right before settlement: the first seven all pass (it really is legitimate), and only this one can tell it's the second time.
After the fix, the same attack:
| Attack | Before | After | |---|---|---| | Replay a legit payment verbatim | 🔴 200, settled again | ✅ 409 at Mandate/Replay | | Concurrent replay (same nonce at once) | 🔴 possible double-spend | ✅ unique constraint settles once | | Normal payment (fresh nonce each time) | 200 | ✅ 200 (no false positive) |
Authorization ≠ this execution being new. A mandate authorizes a class of payments (amount, payee, purpose), but each execution must be uniquely identified and single-use — otherwise one nod equals unlimited payments.
Attack 2: the ledger fingerprint lived only in our own house (closing Episode 4's hole)
Episode 4 gave the audit log a hash chain: delete or edit any entry and the chain breaks, caught on verification. But we conceded a premise: to do it invisibly, an attacker would have to recompute and rewrite every entry after it — and the only place that could prove what the fingerprint was before the rewrite was our own database.
Evidence inside a single trust domain isn't evidence. If we rewrite the whole ledger and then we ourselves say "this was always how it looked," that's not far from having no defense at all.
The patch: anchor the fingerprint somewhere we don't control
Two things:
- A public fingerprint endpoint
GET /api/audit-anchor: returns the current hash-chain tip. Anyone — a monitor, an investor, a passer-by — can record it periodically. - An independent system witnessing it on a schedule: a GitHub Actions job fetches that fingerprint every 4 hours (6×/day) and records it into GitHub's run history + stored artifacts. GitHub is a completely different trust domain from our database (different company, different permissions, different admins).
Now, to rewrite audit history undetectably, you'd have to compromise two independent systems at once: our database and GitHub's records. And once the fingerprint has been witnessed externally even once, rewriting the tail won't match that record — it can't hide.
What this fix does NOT do (as always, stated plainly)
GitHub is still a company we hold an account with. Truly "nobody can change it" means anchoring the fingerprint to a public blockchain (e.g. Bitcoin via OpenTimestamps, or writing it into calldata over our existing Base Sepolia settlement path). That's the level where not even we — nor GitHub — can deny it. We're not pretending we've done that yet — it's the next step. Each layer is harder to repudiate than the last; we're pushing outward one layer at a time, not claiming we reached the end in one leap.
Three lessons
1. Idempotency is a payments floor, not a bonus. We treated it as a "later" optimization; it was a double-spend hole you could exploit on repeat. For anything that moves money, the first question to ask is: what happens if the same request arrives twice?
2. Evidence you can alter only counts when someone else can't be fooled. Tamper-resistance was never about "locking it down"; it's about "someone outside is watching." A log with no external witness is a love letter you write to yourself.
3. We've broken our own gateway five episodes running. Blocked six, missed cumulative limits, found the foundation empty, could erase the log, could pay twice. That isn't the embarrassing part — refusing to publish it would be. Whether a standard is worth trusting isn't about how complete it claims to be; it's about whether its authors publish the results when they break it themselves — including these two: "we could have paid twice, and we could have rewritten the books."
Next episode
We'll push the ledger fingerprint onto a public chain, so that not even we or GitHub can deny it; and we'll re-audit the settlement path itself: does the real on-chain tier have its own replay / reconciliation edges?
Attack it yourself: oceanalt.com/firewall · Test with your own agent: integration guide · See the fingerprint: /api/audit-anchor
(OceanAlt Attack Lab · research note)
Provenance & status
- Byline
- OceanAlt Editorial
- First published
- 2026-07-20
- Last updated
- 2026-08-01
- Source material
- Source not labeled

