diff --git a/SPEC.md b/SPEC.md index ba21956..1d5f886 100644 --- a/SPEC.md +++ b/SPEC.md @@ -184,6 +184,5 @@ shipped since this list was written.) Add when someone asks. states, autoplay default, `run.sh`. 7. Auth — login screen, then multi-user CLI-created accounts with per-user history + test suite. -8. Later (see git log, `docs/CHANGES-2026-09.md`): provider layer with - hianime primary, SQLite storage, stats/events, 2026-09 security - audit batch. +8. Later (see git log): provider layer with hianime primary, SQLite + storage, stats/events, 2026-09 security audit batch. diff --git a/docs/CHANGES-2026-09.md b/docs/CHANGES-2026-09.md deleted file mode 100644 index fc48ca7..0000000 --- a/docs/CHANGES-2026-09.md +++ /dev/null @@ -1,52 +0,0 @@ -# Code-audit fix batch (2026-09) - -Findings from the 2026-09-15 code audit, in shipping order. The systemd -cutover was explicitly deferred — the process stays on run.sh's nohup -model; only the restart mechanism's blast radius is fixed here. - -## 1. login handler blocks the event loop (HIGH) - -`login` is `async def`, so the anti-brute-force `time.sleep(0.5)` inside -the lockout check runs ON the event loop: every wrong password stalls -every in-flight request (all streams included) on the single worker. -Fix: make the handler a plain `def` (uvicorn runs it on the threadpool); -move the lockout logic into a side-effect-free `server/auth.py` so it is -unit-testable without booting the app. Regression test measures -`/api/cdn` latency while a wrong-password login is mid-sleep. - -## 2. run.sh global pkill (HIGH) - -`pkill -f "uvicorn .*app:app"` also matches the test server -(`uvicorn server.app:app --host 127.0.0.1 --port 8399`): an ani-web -restart kills running test sessions (and vice versa). Fix: PID file -(`server.pid`) becomes the primary kill path; any pattern fallback is -port-scoped to the real instance. - -## 3. lockout is per-username only (MEDIUM) - -Spreading failures across usernames bypasses the per-username lockout -entirely (and anyone can lock a known account for 15 min). Add a -per-IP failure budget (`ANIWEB_IP_FAILS` / window) on top of the -per-username lockout. Client IP = `request.client.host`; the reverse -proxy's `X-Forwarded-For` is consulted only when `ANIWEB_TRUST_XFF=1` -(set by run.sh), so a spoofed header can't widen or narrow the gate. - -## 4. login is exempt from the CSRF header check (MEDIUM) - -The custom-header check that makes cross-site form POSTs fail excludes -`/login`. The exemption is load-bearing only by accident (JSON body -content-type); login becomes covered by the same check as every other -mutation, and login.html sends the header. - -## 5. dependencies unpinned (MEDIUM) - -`curl_cffi` version churn = impersonation churn = silent upstream 403 -waves. requirements.txt becomes a full freeze; bump deliberately. - -## 6. live-upstream e2e gated off by default - -The Playwright tests scrape and play real provider content — they go red -whenever a provider rotates or a title is taken down (observed repeatedly). -A suite that fails for non-regressions can't gate anything, so the default -run is now hermetic only; run the live e2e deliberately with -`ANIWEB_LIVE_TESTS=1`.