## Release v0.51.267 — Release II (stage-r17)
Security hardening cluster — 3 @zapabob PRs (forwarded-header trust + TTS prosody validation).
### Security
| Issue/PR | Author | Hardening |
|----------|--------|-----------|
| #3640 | @zapabob | `/api/tts` per-client throttle no longer trusts `X-Forwarded-For` by default (can't spoof to evade the rate limit); forwarded IP honored only behind a trusted-proxy opt-in. |
| #3642 | @zapabob | CSRF same-origin check no longer trusts `X-Forwarded-Host`/`X-Real-Host` by default (closes a forwarded-host CSRF bypass); opt-in keeps legit reverse-proxy deploys working; default uses the real `Host`. |
| #3643 | @zapabob | Browser-provided TTS prosody (rate/pitch/volume) validated against the `±N%` / `±NHz` grammar before `edge_tts.Communicate`. |
### Attribution
Each contributor branch was **rebased onto current master and pushed back to @zapabob's fork** (native authorship preserved), so the source PRs are current/mergeable. Shipped here as one release because all three add a `[Unreleased]` CHANGELOG entry at the same location (merging individually would force a rebase-cascade). Source PRs #3640/#3642/#3643 closed as merged-via-release with credit.
### Gate
- Full pytest suite: **7779 passed, 0 failed**
- ruff: CLEAN
- revert-guard: PASS (all 3 branches rebased; master is an ancestor)
- Codex (regression): **SAFE TO SHIP** — each hardening is **default-secure AND opt-in-compatible** (no legit reverse-proxy/tunnel deploy breaks on update): CSRF forwarded-host default-off + opt-in works + normal same-origin still passes; TTS prosody rejects out-of-grammar input, legit `+N%` passes; TTS throttle ignores spoofed XFF by default.
Co-authored-by: zapabob <1920071390@campus.ouj.ac.jp>
The 2 endpoint tests passed in isolation but flaked in the full suite: a prior
test can leave is_auth_enabled() True globally, so the no-cookie requests got a
401 before reaching the method/length/voice/rate-limit guards under test. The
autouse fixture now monkeypatches api.auth.is_auth_enabled -> False (the handler
imports it at call time) so the validation-path assertions are deterministic.
Both advisors (Opus MUST-FIX + Codex SHIP-ONLY-WITH-FIXES) caught that the Edge
TTS playback was broken despite the settings selector working:
1. ui.js _playEdgeTts used new Audio('/api/tts?text=...') — a GET — but /api/tts
is POST-only (405) and registered only in handle_post. The per-message speaker
button + auto-read silently failed in edge mode, and the GET leaked message
text into the query string/access log. Rewritten to POST JSON + blob object
URL (mirrors the working boot.js path), and now surfaces server errors
(503 not-installed, 429 rate-limit) via toast instead of silent dead air.
2. boot.js hands-free Edge audio was a local var never assigned to the shared
_playingEdgeAudio handle, so stopTTS() (from _deactivate) couldn't stop it.
Now registered + cleared on end/error.
3. Test isolation: the _tts_limiter function-attribute singleton persisted across
the whole suite, flaking 2 of my endpoint tests in the full run. Converted the
reset to an autouse fixture (before+after each) + unique per-test client IPs.
Maintainer review changes on the Edge TTS PR:
- edge-tts was added as a HARD base requirement, but the /api/tts handler is
designed optional (ImportError returns 503). Moved it out of requirements.txt
with an OPTIONAL comment + install hint; updated the 503 message from the
nonexistent 'see docs' to the actual install command. Keeps minimal base deps.
- PR shipped NO tests for a new auth+rate-limited+allowlisted network endpoint.
Added tests/test_issue2931_edge_tts_endpoint.py covering method (405), missing
text (400), over-length (400), voice allowlist (400), per-client rate limit
(429) — all in-process, no real synthesis/network.