|
| 1 | +# Liber — Code Audit (2026-05-31) |
| 2 | + |
| 3 | +A multi-dimension audit (security, correctness, architecture, data/migrations, |
| 4 | +tests/CI, ops, product/UX/a11y, performance) of the Cloudflare Pages app. Each |
| 5 | +finding was grounded in real code and adversarially re-verified. Severities are |
| 6 | +the post-verification ratings. |
| 7 | + |
| 8 | +## Fixed |
| 9 | + |
| 10 | +### Security & cost (commits 1792e2c, 0e380f0, 387c5b8) |
| 11 | +- **CORS info-disclosure** — `functions/api/[[route]].ts` reflected an arbitrary |
| 12 | + `Origin` with `credentials:true`, letting any third-party site read a logged-in |
| 13 | + user's private JSON (`/api/auth/me`, `/api/ai/conversations`). Now an explicit |
| 14 | + allowlist (liber-99x.pages.dev + preview subdomains + localhost + `ALLOWED_ORIGINS`). |
| 15 | +- **Privilege escalation** — any wallet user could self-mint a CLI token |
| 16 | + (`/auth/cli/token`) that `platformAuth`/`adminAuth` treated as `ADMIN_TOKEN`. |
| 17 | + Platform/graph (infra/cost) endpoints now require `ADMIN_TOKEN` (constant-time) |
| 18 | + or a CLI token from an `ADMIN_WALLETS`-listed wallet. Book *publishing* still |
| 19 | + accepts any CLI token (its intended use). |
| 20 | +- **SSRF** — `/books/ingest` fetched an unvalidated `sourceUrl`. Now restricted to |
| 21 | + an https public-domain host allowlist (`INGEST_HOSTS` to extend); private/loopback |
| 22 | + hosts rejected. |
| 23 | +- **Wallet-login replay** — `/auth/verify` did not bind the signed message to the |
| 24 | + nonce. It now requires the message to equal the exact server template |
| 25 | + (`loginMessage`), and the nonce is single-use. |
| 26 | +- **AI cost-abuse** — cookieless `/ai/chat` + `/platform/search` ran Workers AI |
| 27 | + unmetered. Now per-IP rate-limited (`AI_RATE_PER_MIN`, default 20/min) via a D1 |
| 28 | + atomic counter (migration `0011_rate_limits`). NOTE: Cloudflare KV is **not** |
| 29 | + usable for this (eventually-consistent reads); Cloudflare's native Rate Limiting |
| 30 | + binding is **not** supported in Pages config. For hard burst limits, enable AI |
| 31 | + Gateway rate limiting (`AI_GATEWAY_ID`). |
| 32 | +- **Translation cache poisoning** — an empty (non-throwing) AI response was cached |
| 33 | + as a real translation forever. `functions/lib/ai.ts` now flags it `error:true` so |
| 34 | + the route skips the cache write. |
| 35 | + |
| 36 | +### User-visible bugs & quality (this batch) |
| 37 | +- **Upvotes always 0** — the feed hardcoded shares to `up:0` and the comments list |
| 38 | + returned the stale `up` column, despite votes existing in the `votes` table. Both |
| 39 | + now merge live `COUNT(*)` (`functions/routes/social.ts`). |
| 40 | +- **Dead library sort** — real books had `readsN`/`liners` hardcoded to 0 and |
| 41 | + `created_at` dropped, so "最多人读 / 划线最多 / 最近上链" did nothing. `listBooks` |
| 42 | + now computes the metrics (distinct readers from `progress`, highlights from |
| 43 | + `highlights`) and sorts in SQL; books carry `createdAt` (`functions/lib/catalog.ts`). |
| 44 | +- **Platform job double-run** — `runPlatformJob` had no claim guard, so the queue |
| 45 | + consumer and `/jobs/drain` could execute the same job twice. It now claims |
| 46 | + atomically (`UPDATE ... WHERE id=? AND status!='running'`, check `meta.changes`) |
| 47 | + (`functions/lib/platform.ts`). |
| 48 | +- **Simplified→traditional wrong chars** — S2T was a lossy reverse of a many-to-one |
| 49 | + map. Ambiguous chars now default to their most-common traditional form (后→後, |
| 50 | + 里→裡, 余→餘) with exception phrases for the rest (皇后→皇后, 公里→公里, 头发→頭髮), |
| 51 | + applied via a placeholder pass so phrase output survives the char map |
| 52 | + (`src/lib/zh-convert.js`). |
| 53 | + |
| 54 | +### Performance & tests (this batch) |
| 55 | +- **First-paint bundle** — `@mysten/sui` (~32 kB gzip) and `@simplewebauthn` were |
| 56 | + in the main chunk. Now dynamic-imported in the sign-in/subscribe handlers |
| 57 | + (`product-onboarding.jsx`, `cli-auth.jsx`, `product-profile.jsx`) → main chunk |
| 58 | + dropped ~91 kB → ~57 kB gzip; the Sui SDK loads only on sign-in. |
| 59 | +- **Tests** — extracted the money/auth verification into `functions/lib/verify.mjs` |
| 60 | + (Stripe HMAC, Sui payment matching, constant-time compare, nonce binding) with |
| 61 | + behavioral tests in `test/verify.test.mjs` — previously zero executed-code coverage. |
| 62 | + |
| 63 | +## Backlog (confirmed, not yet fixed) |
| 64 | + |
| 65 | +- **MED (arch)** — `Reader()` is a ~927-line god component (45 useState / 23 |
| 66 | + useEffect, `src/components/product-reader.jsx`). Biggest future-velocity tax. |
| 67 | +- **MED (tests)** — auth/session, AI parsing, graph echoes, and platform job state |
| 68 | + still have no runtime tests; many `functions/` tests only string-grep source. |
| 69 | + No vitest/miniflare harness. |
| 70 | +- **MED (perf)** — no route-level code-splitting (~20 screens eager); Google Fonts |
| 71 | + render-blocking `@import`; JSON GETs (`/books`, `/charts`) lack `Cache-Control`; |
| 72 | + `getChapters` does serial R2 reads (N+1). |
| 73 | +- **MED (ops)** — no error tracking/alerting (≈4 `console.*` total); the platform |
| 74 | + queue has no dead-letter queue; AI Gateway off by default; `db:migrate` re-runs |
| 75 | + all migrations by hand (safe only while every migration is `IF NOT EXISTS`). |
| 76 | +- **MED (a11y)** — book-grid cards and AppBar nav are `div`/`a`+`onClick` with no |
| 77 | + role/tabIndex/keyboard support. |
| 78 | +- **LOW** — dual catalog source-of-truth (`window.BOOKS` vs `catalog.js`); |
| 79 | + duplicated `profileRef`/seed-fallback helpers; dead `Placeholder` export; |
| 80 | + embeddings ledger records the first vector's dim for all sids; "最近上链" frontend |
| 81 | + sort branch still missing (backend now provides `createdAt`). |
| 82 | + |
| 83 | +Verifier **refuted** one finding: the knowledge-graph pipeline is not |
| 84 | +"misconfigured/inert" — it is a deliberate, consistently flag-gated |
| 85 | +(`GRAPH_ENABLED`) pre-launch feature. |
| 86 | + |
| 87 | +## Methodology |
| 88 | + |
| 89 | +8 parallel auditors read real code and reported findings with `file:line`; every |
| 90 | +critical/high finding was re-verified by an independent adversarial agent against |
| 91 | +the cited code (which down-rated several and refuted one). Deploys are local |
| 92 | +(`npm run deploy`); migrations are applied by hand (`npm run db:migrate`). |
0 commit comments