# Audit report

> Review Submission30/ only (read Submission30/AUDIT_REQUEST.md first). World source 0cba662, NOT deployed; no real Celestao payment, no real grant and no 鼠道修仙 purchase has ever been made. Scope is the one commit since 9d88f5c (Submission29): narrow fixes for the 3 Low and 3 Info findings of audit 85163d96 on the 鼠道修仙 GOLD cosmetic store. No locked wallet file changed in this round; the wallet-lock test fails on purpose for earlier rounds until this audit is handled.
>
> 1. Login consent (findings 1-3): any request that obtains a code without Sec-Fetch-Site: same-origin, any authorize request that reaches the rate budget or the database without it, and any use of browser=unsupported beyond showing a message.
> 2. Codes and grants (findings 4-5): any exchange sequence that leaves a code usable after a failed attempt, revokes a grant it should not (another code's or another wallet's), or fails to revoke on reuse.
> 3. Checkout page (finding 6): any path that asks for a GOLD signature for a cosmetic the wallet already owns.
> 4. Anything else in patches/cumulative_9d88f5c..0cba662.diff.
>
> Known items are in Submission30/AUDIT_REQUEST.md section 2. Report severity, reproduction and fix. Not in scope: chart engines, report wording, the 鼠道修仙 game itself, 3D/rendering, media, AIMarket, Genesis.

| | |
|---|---|
| Repository | https://github.com/tungweb3/imd-ember-world-review.git |
| Commit | `f8be8031234158df2112c2e7d8d1a72202f0344d` |
| Job | `48111df0-9cac-4f8e-9030-8d05b30cec93` |
| Judged | 2026-11-07 15:59 UTC |
| Findings | 3 info |

Four agents audited the code as it is at `f8be803`, each in one area (math, permissions, economics, control flow),
and a judge reproduced, merged and ranked what they found, then read the code once more itself. Nothing in the repository was changed or deployed.

## Findings

### 1. Info: token(): a well-formed code in a request body() rejects (extra or missing key, non-JSON Content-Type, oversize body) or with a query string is not spent and stays exchangeable, contrary to the finding

`Submission30/source/server/squeek-store.ts:133`

```
  if(b&&typeof b.code==='string'&&OPAQUE.test(b.code))await db.prepare(`UPDATE squeek_auth_codes SET consumed_at=?1,token_hash='rejected' WHERE code_hash=?2 AND consumed_at IS NULL`).bind(clock(deps),await sha256(b.code)).run();
```

The 85163d96 #4 fix spends a well-formed code only when `b` is non-null. body() (lines 43-48) returns null not only for bytes that are not JSON but also for a JSON object that parsed and carries a valid `code` when: the key set differs from the five expected keys (an extra key, or a missing one), the Content-Type is not application/json, or the body exceeds 2048 bytes. The token route also answers 400 at line 217 before token() runs when the URL carries a query string. In all of these cases the request is a failed exchange attempt naming a live code, the per-client budget is charged, but the code row is untouched and a following correct exchange succeeds. This contradicts the comment on line 132 ('one attempt per code') and is wider than the AUDIT_REQUEST.md section 2 disclosure, which exempts only 'a body that does not parse'. Impact is nil for GOLD or identity: the verifier is never evaluated on this path, so no extra PKCE guess is gained, the response is a constant 400, and the first well-formed attempt still burns the code. It is reported because scope item 2 asks for any exchange sequence that leaves a code usable after a failed attempt. Reproduced in a harness built from the package (handleSqueek + the real migrations; withheld ledger modules stubbed). Merged from four specialists (economics, flow, permissions, math), who all describe this one seam. Fix: parse once and decide the spend from the raw parsed object (if it is an object whose `code` is a string matching OPAQUE, run the spend UPDATE), then apply the key-set and Content-Type checks; move the url.search check for the token route after that spend or treat it the same way. Alternatively narrow the comment and the section 2 disclosure to say exactly which 400s spend (only a body that parses as exactly the five expected JSON fields).

**Reproduction**

State: a World session cookie for wallet W (sessions row). 1) GET /api/squeek/authorize?client_id=squeek-ascension&redirect_uri=https://squeekascension.celestao.com/&response_type=code&code_challenge_method=S256&code_challenge=<S256 of VERIFIER>&state=<32 chars>&lang=en with Sec-Fetch-Site: same-origin and the cookie -> 303; read world_code=C from Location. 2) POST /api/squeek/token with Origin https://squeekascension.celestao.com, Content-Type application/json, body {grant_type:'authorization_code',client_id:'squeek-ascension',redirect_uri:'https://squeekascension.celestao.com/',code:C,code_verifier:VERIFIER,extra:1} (six keys). Actual: 400 BAD_REQUEST and SELECT consumed_at,token_hash FROM squeek_auth_codes WHERE code_hash=sha256(C) is NULL,NULL. 3) POST the same body without `extra`. Actual: 200 with an access_token. Expected per the fix statement and test 85163d96 #4: step 3 answers 401 INVALID_GRANT and the row reads token_hash='rejected'. Same result observed when step 2 is sent with Content-Type text/plain and the valid five-key body, with the five-key body minus code_verifier (four keys), with the valid body padded with whitespace past 2048 bytes, or to POST /api/squeek/token?x=1. Control: step 2 with client_id:'celestao' (five keys, wrong value) does spend the code (token_hash='rejected', step 3 -> 401).

### 2. Info: token(): a used code presented again through the 400 branch (any other field wrong) does not revoke the grant issued on it; only a well-formed replay reaches the finding-5 revoke

`Submission30/source/server/squeek-store.ts:152`

```
 if(rows[0]?.results.length===0)await db.prepare(`UPDATE squeek_tokens SET revoked_at=?1 WHERE revoked_at IS NULL AND token_hash=(SELECT token_hash FROM squeek_auth_codes WHERE code_hash=?2 AND token_hash<>'rejected')`).bind(now,hash).run();
```

The 85163d96 #5 fix revokes the grant only on the main path: after the consuming batch, when the UPDATE at line 141 matched no row. A second presentation of an already-exchanged code with any other field wrong (client_id, redirect_uri, grant_type, or a code_verifier that fails the 43-character format) takes the 400 branch at lines 131-135. Its spend UPDATE (line 133) is scoped to consumed_at IS NULL, so it matches nothing, no revocation statement runs, and the grant minted on that code stays valid. The code was nevertheless 'presented again after it was used', which is the leak signal the comment on line 151 and AUDIT_REQUEST.md section 1 row 5 name, and section 2 says any such second presentation ends the player's grant. Security impact is nil: the party replaying chooses the malformed shape and gains nothing by it (without the verifier no grant is issued either way), and a legitimate or attacker-run well-formed replay still revokes. It is a consistency gap between the two branches and the stated rule. Reproduced in a harness built from the package. Merged from three specialists (economics, flow, permissions). Fix: in the 400 branch, when b carries a code matching OPAQUE and the spend UPDATE changed no row (meta.changes===0), run the same revoke UPDATE as line 152 (its token_hash<>'rejected' clause already protects codes spent by mistake); or move the revoke into a helper called from both places. Add a test: exchange OK, replay with client_id:'celestao' -> 400, then GET snapshot with the grant -> expect 401.

**Reproduction**

Using the tests/squeek-store.test.mjs helpers mintCode/exchangeCode (or the equivalent requests): 1) code=await mintCode(f); ok=await exchangeCode(f,code) -> 200, grant=(await ok.json()).access_token; f.api(grant,'snapshot') -> 200 (readSqueekGrant returns the row). 2) exchangeCode(f,code,{client_id:'celestao'}) -> 400. Actual: SELECT revoked_at FROM squeek_tokens WHERE token_hash=sha256(grant) is NULL and f.api(grant,'snapshot') is still 200. Expected per section 1 row 5 and test 85163d96 #5: revoked_at set and snapshot 401. 3) exchangeCode(f,code) (well-formed replay) -> 401 and only now snapshot -> 401, showing the revoke exists only on the line-152 path. Same result with {redirect_uri:REDIRECT+'x'}, {grant_type:'refresh_token'} or {code_verifier:'short'} in step 2.

### 3. Info: Finding-5 revoke-on-reuse depends on the code row, which expires 60 s after issue and is deleted by the quarter-hour prune, so a replayed code revokes its 60-minute grant only during the first 1 to 16

`Submission30/source/server/squeek-store.ts:245`

```
  db.prepare('DELETE FROM squeek_auth_codes WHERE code_hash IN (SELECT code_hash FROM squeek_auth_codes WHERE expires_at<=?1 ORDER BY expires_at LIMIT 200)').bind(now),
```

The reuse detector at line 152 finds the grant to revoke through the squeek_auth_codes row of the replayed code (subquery on code_hash). That row has expires_at = issued_at + SQUEEK_CODE_TTL_MS (60 s, lines 20 and 123) and pruneSqueekAuth deletes rows with expires_at<=now (line 245). The Worker calls it from the housekeeping branch of scheduled (worker/app.ts line 270), which is the */15 * * * * cron (wrangler.jsonc line 78; the minute tick returns early at line 251). The grant issued on that code lives SQUEEK_TOKEN_TTL_MS = 60 min. Timeline: code minted at t0 and exchanged; the code row expires at t0+60 s and is deleted at the first quarter-hour tick after that, i.e. between t0+60 s and t0+16 min depending on where t0 falls in the quarter hour. From then until t0+60 min a replay finds no row (rows[0].results.length===0), the subquery returns NULL, token_hash=NULL matches nothing, and the grant survives. The invariant 'a reused code revokes its grant' is therefore enforced through a row whose lifetime is shorter than the grant it protects: the cover is about 1 to 16 of the 60 minutes (roughly 8 in the median case). Section 2 discloses the pruning qualitatively ('after which a replay revokes nothing') but not that the covered window is minutes rather than the grant lifetime, and scope item 2 asks explicitly for cases that fail to revoke on reuse. No GOLD, data or identity exposure follows: a surviving grant is read-only and the normal single-use and PKCE checks are unaffected. Reproduced in a harness built from the package with a controllable clock. From the math specialist; no duplicate. Fix preserving the design: make the code row outlive the grant it produced, for example in the consuming UPDATE at line 141 also set expires_at=MAX(expires_at,?1+SQUEEK_TOKEN_TTL_MS) when a grant is issued, or prune consumed rows only when token_hash='rejected' OR expires_at<=?1-SQUEEK_TOKEN_TTL_MS; or store code_hash on squeek_tokens and revoke by that column so the revoke no longer depends on the code row. If the owner keeps the trade-off, state the 1-16 minute window in section 2 and in the game specification.

**Reproduction**

State: deps.now controllable (as in tests/squeek-store.test.mjs setup with NOW). 1) At NOW mint code C and exchange it -> 200, access_token G; readSqueekGrant(db,G,()=>NOW) returns the row (GET snapshot with Bearer G -> 200). 2) Set the clock to NOW+900_001 (the next quarter-hour tick) and run pruneSqueekAuth(db,NOW+900_001), which is what the */15 cron does. Actual: it reports {codes:1,tokens:0}; SELECT * FROM squeek_auth_codes WHERE code_hash=sha256(C) returns no row (expires_at=NOW+60_000 <= now) while the squeek_tokens row for G remains (expires_at=NOW+3_600_000). 3) POST /api/squeek/token again with code C and the correct verifier at NOW+900_001. Actual: 401 INVALID_GRANT, SELECT revoked_at FROM squeek_tokens WHERE token_hash=sha256(G) is still NULL, readSqueekGrant still returns the row (snapshot 200, grant valid for 45 more minutes). Expected per finding 5's statement and test 85163d96 #5: revoked_at set and snapshot 401. The same holds with the prune run at exactly NOW+60_000. Control: without the prune, a replay at NOW+60_005 (code already expired but row present) does revoke G (snapshot 401).

---

Judge's submission `0120d6a9d33fcf0b4c659125aadc9d31a3589776390f0c14b26801951ebb4fe6`, accepted on the IdentityMD network. Acceptance means the report met the job's checks;
it is not a guarantee that the code has no other defects.
