What people found,
and what it cost us.
Our policy says every paid report gets published with its amount. This is that page. It carries the finding, the fix, the transaction that paid for it, and the parts that stayed open, because a page of victories would not be worth reading.
The sign-in nonce was never spent, so one captured signature could be replayed into new sessions
Found by JustGH · credited exactly as asked, plain text, no links
Signing in asked a wallet to sign a nonce, then checked that nonce for age and nothing else. It was never marked as used, so the same wallet, nonce and signature could be posted to /api/auth/verify again and again, and each attempt minted a fresh session cookie. The reporter demonstrated three replays in a row, and a fourth after the victim had pressed sign out.
Anyone who obtained one signed sign-in message, by phishing it on a look-alike page or from any leak of the request, could hold a session as that wallet for the length of the replay window. A session reads /api/rewards, which returns that wallet's order history: what was swapped, for how much, and when. On a router that sells privacy, that is the part that matters. No funds path existed: DarkRoute holds no balances and a session cannot move anything.
- The nonce is bound to the wallet it was issued for, so it cannot be carried to another address.
- It carries twelve random bytes, so two issued in the same millisecond stay distinct.
- It is spent on first use and remembered until it expires, so a second attempt is refused even with a valid signature.
- The window is two minutes instead of ten.
- The message a wallet displays now names the domain and its own expiry, so a signature requested by another site reads as one.
- Bad input to /api/order came back as a blank 500 rather than a 400 with a reason: a zero recipient address and an absurd amount both reached the venue and were refused there. Fixed the same day; those now answer 400 with the venue's own words, and a venue outage answers 502.
- The reporter noted that our warning about the single-instance assumption lived only in a code comment, where nobody configuring a second container would ever read it. He was right. It now leads app/RUNBOOK.md, the deploy script and the env example, and writing it turned up a second thing that breaks quietly at instance two: the rate limiter is per process, so the limits printed on our public API page would silently stop being true.
- One item in the report did not reproduce, on either side: /api/waitlist rejecting hidden kinds. The reporter reran it himself and closed it rather than leaving it in.
- · Nothing from this report. Session revocation, which both sides agreed was a separate issue rather than part of the finding, shipped the same night anyway: signing out now ends every session for that wallet instead of clearing one browser, and session lifetime went from seven days to two.
- · Known and stated rather than hidden: spent nonces are held in the app process, which is correct for one container and wrong the day a second one runs. The runbook says so and names the fix.
- 12 Sep, 15:48
- Reported by email to support@, privately, with a working proof of concept
- 12 Sep, same evening
- Reproduced on production with a throwaway wallet before anything was touched
- 12 Sep, same evening
- Fixed, deployed, and the reporter's own retest confirmed it
- 12 Sep, late
- Session revocation shipped, beyond the scope of the report
- 13 Sep, 10:52
- Paid, 0.0425 ETH
- 13 Sep
- Published here
$105, 0.0425 ETH on Robinhood Chain, a little over the $100 agreed because we rounded up.
robinhoodchain.blockscout.com/tx/0x8650183ccf68d78f1d68dbdfe440739eeef0c8659458dc8ba6bba2b67eb312ecThe next one gets the same page.
Same rules, same publication, same receipt. Rewards are sized to what a router with almost no revenue can afford, and we would rather tell you that than quote a number we cannot pay.
Scope and channels →