fix(api): Origin-Guard — Browser erreichen nur POST /speak von gelisteten Origins

CORS allein verhindert nur das Lesen der Antwort, nicht die Wirkung: Ein
POST mit text/plain-Body oder ein Formular-POST geht ohne Preflight raus,
und talkd hat den Body trotzdem als JSON gelesen bzw. /mute, /cancel und
/mcp ohne Body ausgeführt. Jede Webseite konnte so sprechen, muten und
abbrechen.

Neue Middleware vor dem Router: Anfragen mit Origin-Header kommen nur als
OPTIONS /speak (Preflight, ohne Wirkung) oder POST /speak von gelisteter
Origin durch; alles andere ist 403, bevor ein Body gelesen wird. CLI, curl
und MCP-Clients senden keinen Origin und sind nicht betroffen.

Prozesstests angepasst (ungelistete Origin auf /speak → 403, Steuer-
Endpunkte aus dem Browser → 403) und TestBrowserKannNichtSteuern ergänzt,
der den Angriff mit simple requests durchspielt und prüft, dass weder
Provider noch Mute-State berührt werden.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
m3ta-chironandClaude Fable 5.1 committed 2026-10-24 09:35:45 +02:00
1 parent 8909f37802
commit 46bd507fd8
4 files changed
+132 -20

No files matched your search

+11 -2
View File
@@ -126,8 +126,17 @@ allowed_origins = ["https://chat.az-gruppe.com"] # Env TALK_ALLOWED_ORIGINS
`Access-Control-Allow-Origin` (die Origin bzw. `*`), `-Methods: POST,
OPTIONS`, `-Headers: Content-Type`, `Max-Age: 600`.
- Echter `POST /speak` mit gelisteter `Origin` trägt denselben ACAO-Header.
- Ungelistete Origins bekommen saubere `204`/`202` **ohne** CORS-Header —
die Blockade macht der Browser.
- **Die Tür ist serverseitig zu, nicht nur browserseitig.** CORS allein
verhindert nur das *Lesen* der Antwort; einen POST mit `text/plain`-Body
oder einen Formular-POST schickt jede Webseite ohne Preflight ab. Deshalb
lehnt talkd jede Anfrage mit fremdem `Origin`-Header mit `403` ab, die
nicht POST /speak von einer gelisteten Origin ist — also ungelistete
Origins auf /speak, jeden Browser-Zugriff auf `/status`, `/mute`,
`/unmute`, `/cancel` und `/mcp`, auch von gelisteten Origins. Nur der
Preflight `OPTIONS /speak` bleibt für ungelistete Origins ein `204` ohne
CORS-Header (ohne Wirkung; der Browser verwirft den geplanten POST).
CLI, curl und MCP-Clients senden keinen `Origin`-Header und sind nicht
betroffen.
**MCP** (`POST /mcp`, Streamable HTTP): schlanke eigene Implementierung
des JSON-RPC-Subsets ohne SDK — `initialize` (angefragte