Commit Graph
4 Commits
Author SHA1 Message Date
davideandClaude Opus 5 907e32e9e0 Make X-Forwarded-For trustworthy instead of attacker-controlled (B-54)
client_ip() read the first element of X-Forwarded-For, which is correct only if
the proxy replaces the header. Caddy appends the peer address to whatever the
client sent, so element 0 was whatever the caller claimed: rotating a fake value
per request minted a fresh identity every time and walked straight through the
login and registration throttles (B-33) and the SSE per-IP subscriber cap
(B-38). Only the per-username login bucket, which doesn't key on the IP, still
bit.

Both halves of the audit's fix, since they hold independently:

- the Caddyfile overwrites the header with `header_up X-Forwarded-For
  {remote_host}`, so what reaches the app is the actual peer and nothing else.
  This is the one that makes the app's assumption true at the source.
- client_ip() reads the *last* hop rather than the first — the element written
  by the hop closest to us, i.e. by our own proxy. Exactly one trusted proxy
  sits in front of the app (`app` is only `expose`d on the compose network,
  never published to the host), so that element is the real peer.

An empty or comma-only header now falls back to request.client.host instead of
returning "", which was its own shared-bucket evasion.

Regression tests both sides: two requests spoofing different prefixes must key
to the same IP, and the Caddyfile must keep the header_up directive (checked by
`caddy validate`).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 22:14:03 +02:00
davideandClaude Sonnet 5 d60da11603 Add baseline HTTP security headers in Caddy (B-43)
Caddy adds none of these on its own. Add HSTS, X-Content-Type-Options,
X-Frame-Options, Referrer-Policy and a CSP scoped to default-src
'self' plus the one external asset (Google Fonts). script-src/style-src
need 'unsafe-inline' because both SPAs rely on inline onclick handlers
and style="" attributes throughout — removing those is a separate,
larger refactor.

Validated with `caddy validate` and a live container curl check.
Adds a static regression test asserting the header directives stay
present in the Caddyfile, since nothing else in the Python suite
exercises it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 15:44:47 +02:00
davideandClaude Sonnet 5 f229f91632 Add server-push (SSE) notifications for round/bet/balance state changes
Frontend dashboards previously found out about state changes only on their
next poll tick (up to 15s, or 3s during a draw) — this adds a push channel
so updates land as soon as they happen instead.

- app/rounds/events.py: a small in-process pub/sub (RoundEventBroadcaster).
  The message carries no payload — it's just a "something changed, go
  refetch" signal, so it needs no auth and no knowledge of who's allowed to
  see what; personalization stays entirely in the existing REST endpoints.
- GET /rounds/stream: an SSE endpoint exposing that channel, with keep-alive
  comments so it survives idle periods behind a reverse proxy, and a
  defensive MAX_SUBSCRIBERS cap (well above the ~100 concurrent users
  expected) — past it, the endpoint returns 503 instead of opening a stream,
  and callers just keep working off polling.
- publish() calls added at every point that actually changes what a
  dashboard would want to know: new round opened, round status transitions
  (closing/drawing/paying_out/closed), a bet or withdrawal broadcast, any
  pending tx confirming (bet/withdrawal/payout), a deposit credited, and a
  new block tip arriving (the exact moment the "drawing" phase is waiting on).
- Caddyfile: excludes /rounds/stream from gzip encoding, since compression
  would buffer output and defeat the point of a live stream.

Single-process only by design for now (no cross-worker fan-out) and the
notification is a generic broadcast rather than a per-user channel — both
are deliberate scope decisions for the current ~100-user, single-container
deployment, not oversights. Polling is left fully in place as a fallback;
this is purely additive.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 10:52:07 +02:00
davideandClaude Sonnet 5 d35784d574 Add docker-compose stack: app + Caddy reverse proxy with automatic TLS
Caddy's site address comes from SITE_ADDRESS (defaults to "localhost").
Left at that default, Caddy detects it isn't a public hostname and
issues a self-signed cert from its own internal CA — no domain needed
for local/dev testing. Set to a real domain, it gets a genuine Let's
Encrypt certificate automatically instead.

DB, encrypted master key and logs are bind-mounted from ./data/ on the
host (not opaque Docker-managed volumes), so they survive container
restarts/rebuilds and stay reachable for manual inspection/backup
directly from the repo root. ./data/ is gitignored.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 11:15:04 +02:00