Skip to content

Security: rthway/45-English

Security

docs/SECURITY.md

Security

The controls this application implements (section 53). For the findings of the last review pass, see SECURITY_REVIEW.md.

Authentication

  • Passwords — bcrypt at cost 12 over a SHA-256 pre-hash, so passphrases longer than bcrypt's 72-byte limit keep their full entropy.
  • Access tokens — short-lived JWTs (30 minutes by default), held in memory by the SPA so a stolen localStorage entry cannot be replayed.
  • Refresh tokens — opaque, stored hashed, rotated on every use, and revoked on logout, on password reset, and on "log out everywhere". A replayed refresh token is rejected.
  • Cookies — httpOnly, SameSite=Lax, and Secure in production.

Account enumeration

Login returns an identical status and body for a wrong password and an unknown account. Password reset always answers 202 whether or not the address exists.

Authorisation

Every learner-scoped route re-checks ownership and returns 404 rather than 403 for someone else's resource, so an ID is never confirmed to an attacker. Admin routes require is_admin; tests assert each one rejects anonymous (401) and non-admin (403) callers.

An admin cannot remove their own admin flag — an install can never be left with no administrator.

Input handling

  • Pydantic bounds every string length and numeric range at the edge.
  • Audio uploads are capped at 25 MB (and again in nginx).
  • Prompt names are restricted to ^[a-z0-9_]{1,64}$ before any path is built, so the admin prompt editor cannot write outside its directory.
  • All database access goes through SQLAlchemy constructs; there is no string-built SQL anywhere in the codebase.

Rate limiting

Per-IP fixed window, Redis-backed with an in-process fallback: 10/min on auth, 60/min on speech and session turns, 20/min on destructive privacy actions.

Without REDIS_URL the limiter is per-process — correct for a single worker, but set Redis in multi-worker production.

Transport and headers

Every response carries X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: no-referrer and Permissions-Policy: microphone=(self); HSTS is added in production. nginx repeats these and forwards X-Forwarded-* so the app sees the real scheme and client IP.

Secrets

AI and speech keys are backend environment variables. They are never sent to the browser and never returned by the admin API — a test asserts the string api_key never appears in the provider status response.

SECRET_KEY signs access tokens. Rotating it invalidates every session by design; keep it in a secret store, not in the image.

Audit

Registration, login, logout, password reset, every privacy action and every admin change are recorded with IP and user agent.

Data protection

Audio is stored only on explicit opt-in and deleted after the retention window; account deletion requires the password and cascades to every owned row. See PRIVACY.md.

Known limitations

  • CSRF — the SPA uses bearer tokens and cookies are SameSite=Lax, which blocks cross-site POSTs. No CSRF token is issued. A future client that relies on cookie auth for cross-origin state changes would need double-submit tokens.
  • Password reset delivery is a pluggable notifier; in non-production the token is returned in the response for testing.
  • Content Security Policy is not yet set. Adding a strict CSP in nginx is the natural next hardening step.

Reporting

Security issues should go to the maintainer privately rather than in a public issue.

There aren't any published security advisories