v1.21.2 — Security & robustness hardening (ADR-093)
🔒 Security & robustness release
A systemic audit closed the two highest-severity items left in the backlog, and the math-rendering loop is now solid end to end. No DB schema change, no migration, no new env vars.
Trusted proxy chain (F1, ADR-093)
The prod API port was published on every host interface ("8000:8000" — LAN clients could bypass Cloudflare) and uvicorn ran without --proxy-headers: request.client.host was the Docker gateway for every request, so the per-IP rate limit was one shared global bucket (one aggressive client could 429 everyone), GeoIP always resolved "local", and the auth path read a spoofable raw X-Forwarded-For. Now, as one coupled invariant: ports 8000/9091 are loopback-bound (cloudflared = single public entry; compose-internal SSR/scrape traffic unaffected), uvicorn runs --proxy-headers --forwarded-allow-ips="*" (safe only because of the loopback binding — documented at both sites), and request.client.host is the single client-IP source everywhere.
XSS sanitization boundary (FF-XSS, ADR-093)
The chat markdown pipeline ran rehype-raw with no sanitizer — the LLM can relay verbatim third-party HTML (email bodies, fetched pages, MCP output) that executed with the user session. rehype-sanitize is now the boundary (rehypeRaw → rehypeSanitize → rehypeKatex), schema audited against every legitimate producer (cards, callouts, data-* buttons, MCP sentinels, tel:, freed className on the 7 tags defaultSchema constrains). script/iframe/form/handlers dropped; MCP/Skill Apps never go through markdown (sentinel → sandboxed widget).
Math rendering, closed end to end
- A pre-existing crash — curly braces from LaTeX/MCP history breaking the prompt f-string template (
{2}→ValueError), freezing every follow-up turn on "Generating response…" — is fixed (escape_braceson the base system prompt). - Formulas render whatever notation the model picks:
$…$,$$…$$,```latex/```mathfences,\[…\],\(…\); dollar amounts stay literal (MathJax delimiter rules; code blocks skipped). The response prompt no longer teaches the model to wrap formulas in backticks.
Robustness riders
- State schema migrations wired (F7): the migration chain (zero callers since schema 1.0) now runs on every checkpoint load; new 1.2→1.3 step formalizes the ADR-092 replay-safe HITL keys +
user_display_name. - State persistence by contract (F5): response-node writes go through the state update (reducer-equivalent) instead of shared-reference mutation.
- Pure-ASGI middleware (F28): the 4 custom middlewares no longer spawn a per-request task-group + re-wrap every SSE chunk (PrometheusMiddleware untouched).
- Language-agnostic HITL token guard (FF2): structural
hitl_id prefix instead of hardcoded French sentences.
Docs & quality
ADR-093 (+ index entries), SECURITY.md, RATE_LIMITING.md, ARCHITECTURE.md (pure-ASGI), STATE_AND_CHECKPOINT.md (v1.3 chain wired); in-app FAQ changelog, HOW guides and the security blog article updated in all 6 languages. Ruff / Black / MyPy strict clean; backend 8518 tests green (+17), frontend 118 green (+18); every commit passed the full pre-commit hook.
Post-deploy validation (prod): app reachable via the tunnel; curl 192.168.0.14:8000 from the LAN refused; real public IPs in logs and Grafana.
Full changelog: https://github.com/jgouviergmail/LIA-Assistant/blob/main/CHANGELOG.md