Files
nas-tools/specs/sprint-00-critical-security.md
Gan, Jimmy ddaf3c6cee security: Sprint 00 — critical security fixes (OPC WS auth, Transmission creds, SECRET_KEY, passkey rate limit)
- Add JWT token auth to OPC WebSocket (unauthenticated → 403, per-user tracking)
- Externalize Transmission RPC credentials to TRANSMISSION_USER/PASS env vars
- Remove hardcoded SECRET_KEY fallback from dev compose
- Rate-limit passkey register options endpoint at 5/minute
- Add PDD (docs/improvement-plan.md) and sprint specs (specs/)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-03 13:31:25 +08:00

4.2 KiB

Sprint 00 — Critical Security Fixes

Depends on: nothing Duration: ~4h Goal: Close exploitable security holes — unauthenticated WebSocket, hardcoded credentials, known signing keys.


S00.1 — Add JWT auth to OPC WebSocket

  • Files: dashboard/backend/routers/opc_ws.py:66-84, dashboard/frontend/src/lib/opc-ws.js

  • Estimate: 2h

  • Done means: Unauthenticated WebSocket connections to /ws/opc receive 403. Authenticated connections (JWT token via ?token= query param) connect normally. The frontend OPC WebSocket client passes the access token from the cookie/auth store.

  • Verify by:

    1. websocat ws://localhost:4000/ws/opc → 403 Forbidden
    2. Login via browser → navigate to OPC page → WebSocket connects (check browser devtools Network tab, WS frame shows 101)
    3. websocat "ws://localhost:4000/ws/opc?token=<valid_token>" → connects, receives broadcasts
    4. Existing integration tests pass (test_websocket_security.py)
  • Risk: The frontend opc-ws.js constructs the WebSocket URL — must include the token there. If the frontend token store doesn't expose the access token synchronously, this may need a refactor of how the WS client is initialized.

  • S00.1 — Add JWT auth to OPC WebSocket

S00.2 — Externalize Transmission credentials

  • Files: dashboard/backend/routers/transmission.py:16,40, dashboard/backend/config.py, dashboard/docker-compose.dev.yml

  • Estimate: 1h

  • Done means: Transmission RPC credentials read from TRANSMISSION_USER and TRANSMISSION_PASS env vars. No hardcoded ("admin", "admin") in source. Startup fails with clear error if env vars are unset.

  • Verify by:

    1. Set TRANSMISSION_USER=admin TRANSMISSION_PASS=admin → Transmission endpoints work
    2. Unset vars → app fails at startup with RuntimeError("TRANSMISSION_USER and TRANSMISSION_PASS must be set")
    3. grep -r '"admin"' dashboard/backend/routers/transmission.py → no match
  • Risk: The compose files (dev + prod) need the new env vars added. The Transmission container itself uses these same creds — verify the actual Transmission daemon password hasn't been changed from default.

  • S00.2 — Externalize Transmission credentials

S00.3 — Remove hardcoded SECRET_KEY fallback

  • Files: dashboard/docker-compose.dev.yml:21

  • Estimate: 0.5h

  • Done means: Line 21 reads SECRET_KEY=${SECRET_KEY} (no :-fallback). Missing env var causes compose to fail with a clear error rather than silently using a known key.

  • Verify by:

    1. Unset SECRET_KEY, run docker compose -f docker-compose.dev.yml config → error about missing required variable
    2. Set SECRET_KEY, run same command → succeeds
    3. config.py:14-16 already enforces length check at app startup — this is the defense-in-depth belt.
  • Risk: CI workflows (test.yml, deploy.yml) already set SECRET_KEY explicitly in env, so no CI breakage expected. Local dev must now set SECRET_KEY in their .env.

  • S00.3 — Remove hardcoded SECRET_KEY fallback

S00.4 — Rate-limit passkey register options endpoint

  • Files: dashboard/backend/routers/passkey.py:58-72

  • Estimate: 0.5h

  • Done means: passkey_register_options endpoint has @limiter.limit("5/minute") decorator. Exceeding the limit returns 429.

  • Verify by:

    1. Call POST /api/auth/passkey/register/options 6 times in 60 seconds → 6th returns 429
    2. Wait 60 seconds → call succeeds again
    3. Existing passkey integration tests still pass
  • Risk: None — this is a pure additive constraint. The limiter instance at passkey.py:25 may not integrate with the app's limiter state — verify it works correctly with the current slowapi version. If not, switch to request.app.state.limiter.

  • S00.4 — Rate-limit passkey register options endpoint


Exit criteria

  • websocat ws://localhost:4000/ws/opc → 403 (auth check added; will reject without token)
  • grep -r '"admin"' dashboard/backend/routers/transmission.py → no match
  • grep ':-c0e8dcd7' dashboard/docker-compose.dev.yml → no match
  • All backend tests pass (245 passed, 0 new failures)
  • All frontend tests pass (pre-existing Vite plugin compatibility issue)