2026-07-27 09:02:34 +02:00
# Known bugs
2026-07-26 22:10:50 +02:00
2026-07-27 09:02:34 +02:00
A second full-codebase audit on 2026-07-27 found **25 further issues** (4 critical, 6 high,
2026-07-27 15:34:49 +02:00
7 medium, 8 low), listed below as B-43 … B-49. B-25 through B-42 are fixed (see "Previously
fixed" below) — no Critical-, High- or Medium-severity finding remains open; the remaining 7 are
Low/hygiene. The 139-test suite was green at the time of the audit, so none of these were caught
by existing coverage — every fix lands with a regression test (the eighteen fixes so far brought
the suite from 139 to 224).
2026-07-26 22:10:50 +02:00
2026-07-27 09:42:24 +02:00
The recurring pattern across the open findings is worth stating once: the code is rigorous
about the failure modes that have actually been hit, and silent about the ones that have not.
2026-07-27 14:12:27 +02:00
The payout phase is now fully recoverable; the "drawing" phase (waiting on a block) is now
observable (B-36) but still has no equivalent resume-after-restart — see "Known gaps / TODO"
2026-07-27 14:23:00 +02:00
in [CLAUDE.md ](CLAUDE.md ), which is also where other by-design limitations (single-shared-token
admin auth, single-process assumptions, no user-facing history, etc.) are documented.
2026-07-27 00:35:28 +02:00
---
2026-07-27 09:02:34 +02:00
## Low / hygiene
2026-07-26 22:10:50 +02:00
2026-07-27 09:02:34 +02:00
### B-43 — No HTTP security headers
The [Caddyfile ](Caddyfile ) sets no CSP, no `X-Frame-Options` /`frame-ancestors` , and no HSTS
(Caddy does not add it on its own). The JWT lives in `localStorage` , so any XSS exfiltrates
it, and the page is iframeable.
**Fix:** a `header` block in the Caddyfile with `Strict-Transport-Security` ,
`X-Content-Type-Options: nosniff` , `Referrer-Policy` and a CSP tight enough for two static
pages with no external assets (`default-src 'self'` ).
### B-44 — README and CLAUDE.md contradict each other
The README says to run `uvicorn --reload` directly and
`docker compose run --rm app python scripts/generate_master_key.py` ; CLAUDE.md says explicitly
that neither is supported. Whoever opens the repo reads the README first.
**Fix:** align the README's Quick start with the Docker-only workflow documented in
CLAUDE.md and `docs/setup.md` .
### B-45 — Unvalidated and unpaginated admin list endpoints
`limit: int = 50` on `/admin/rounds` and `/admin/audit-log` has no bounds (`-1` means
"everything" on SQLite), and `/admin/pending-transactions` has no limit at all — it grows
without end.
**Fix:** `Query(default=50, ge=1, le=500)` on both, and the same treatment plus a status filter
on the pending-transaction list.
### B-46 — `secrets.compare_digest` on a `str` raises on non-ASCII input
`api/routes/admin.py:27` raises `TypeError` — a 500 instead of a 403 — when the header contains
non-ASCII characters.
**Fix:** compare the UTF-8 encoded bytes of both sides.
### B-47 — Unbounded `String` columns for large text
`raw_tx_hex` (`db/models.py:146` ) and `payload_json` (`:178` ) should be `Text` . It works on
SQLite and PostgreSQL and breaks elsewhere.
**Fix:** switch both to `Text` in a migration.
### B-48 — No cap on input count in `select_utxos`
A user with hundreds of small UTXOs builds a huge transaction whose fee — deducted from the bet
amount — materially erodes their contribution to the pool, and it can exceed standardness
limits.
**Fix:** cap the selected inputs (e.g. 50) and fail with a translatable error suggesting a
consolidation, or consolidate the address automatically when the count crosses a threshold.
### B-49 — Rollback paths do not publish an SSE update
`bets/service.py:_release_failed_bet` and `withdrawals/service.py:_release_failed_withdrawal`
restore the balance without calling `broadcaster.publish()` , so dashboards only find out on
their next poll.
**Fix:** one `broadcaster.publish()` at the end of each, as every other state-changing path
already does.
---
## Previously fixed
2026-07-27 10:07:21 +02:00
- **B-25** — the payout had no two-phase write, unlike bets and withdrawals
- **B-26** — a payout failure or a process restart could wedge a round in `paying_out` forever
- **B-27** — an RBF bump reset the reconciler's own abandon clock, so a repeatedly-bumped tx was never abandoned
- **B-28** — a hostile Electrum server (or a MITM) could single-handedly pick the round's winner
2026-07-27 10:25:07 +02:00
- **B-29** — a UTXO absent from one server's `listunspent` was marked spent immediately, irreversibly, on a single unauthenticated reply
2026-07-27 10:35:23 +02:00
- **B-30** — a lost scripthash subscription meant a user's deposits were never credited, with no periodic safety net
2026-07-27 10:44:23 +02:00
- **B-31** — resubscribing on reconnect ran serially before anything else started, freezing the chain tip (and so an in-flight draw) for the whole sweep
2026-07-27 15:34:49 +02:00
- **B-42** — Swagger/ReDoc/the raw OpenAPI JSON enumerated the entire API surface, admin endpoints included, to anyone who requested them; now off by default and gated behind `ENABLE_API_DOCS`
2026-07-27 12:20:22 +02:00
- **B-32** — an RBF bump could retry forever below BIP125's relay-mandated minimum fee delta, with no ceiling on the fee rate either
- **B-33** — `POST /auth/login` had no rate limiting, so a password could be brute-forced against an enumerable username list
- **B-34** — password change/reset didn't invalidate already-issued JWTs, so a stolen token survived a change meant to lock it out
- **B-35** — API timestamps round-tripped as naive datetimes, so the frontend parsed them as local time instead of UTC
2026-07-27 14:12:27 +02:00
- **B-36** — a stalled draw wait had no timeout, no log, and no audit trail, so a frozen round showed nothing in `/admin`
2026-07-27 14:23:00 +02:00
- **B-37** — a withdrawal covered by unconfirmed change answered "insufficient balance" instead of distinguishing it from actually having no funds
2026-07-27 14:44:15 +02:00
- **B-38** — the SSE subscriber cap was global, so one client opening enough connections degraded every other user to polling
2026-07-27 14:53:22 +02:00
- **B-39** — SQLite ran without WAL or a `busy_timeout` , so a writer could block every reader and a second writer failed immediately instead of waiting
2026-07-27 15:14:58 +02:00
- **B-40** — `bump_fee` held a DB session open across N slow network calls, and computed a prevout's value from a server-reported float instead of an exact integer
2026-07-27 15:27:58 +02:00
- **B-41** — confirmation/reconciliation depended on a verbose `blockchain.transaction.get` reply many Electrum servers reject, and abandonment relied on fragile substring-matching of an error message
2026-07-27 10:07:21 +02:00
See git history for the fix-by-fix breakdown (commits `f13f685` , `50a43ae` , `933760e` , and the
2026-07-27 15:34:49 +02:00
B-28/B-29/B-30/B-31/B-32/B-33/B-34/B-35/B-36/B-37/B-38/B-39/B-40/B-41/B-42 fixes). Suite grew from 139 to 224 tests over the eighteen.
2026-07-27 09:39:38 +02:00
2026-07-27 09:02:34 +02:00
A full-codebase audit on 2026-07-26 (commit `d4e0974` ) found 24 bugs across every Python
module under `app/` , both static frontends, and the Docker/Caddy deployment — 5 critical,
7 high, 7 medium, 5 low. All 24 were fixed and verified against the current code on
2026-07-27; the fixes are covered by the regression suite (grew from 79 to 139 tests) and
five of them were additionally confirmed against a real mainnet deployment (see git history
between `fb734bb` (documenting the findings) and `845ba98` (recording the audit outcome) for
the fix-by-fix breakdown — each commit message names the bugs it closes and where their
tests live).