The webhook before_request guard validates Twilio signatures on /voice/* only (server.py:198-218) — the comment at server.py:204-205 explicitly notes the WS is unguarded. The /ws/call handler (server.py:759-825) accepts any connection on the public port 5050, opens a Deepgram live session, and on any transcribed speech calls the LLM and TTS.
Anyone who can reach port 5050 (internet-exposed by design, README.md:58) can converse with the agent and consume Deepgram/LLM/TTS quota — completely bypassing the PIN gate, lockout, and constant-time compare (server.py:642-665). The PIN guards a door next to an open window.
Fix: mint a one-time per-call token when generating the <Connect><Stream> TwiML (server.py:653-654), pass it via <Stream><Parameter> (arrives in the start event's customParameters), and close any WS connection whose start event doesn't carry a valid token mapped to the expected CallSid. Keep tokens single-use with a short TTL.
Gotchas: _ws_url() (server.py:221-227) has two call sites plus the WEBHOOK_URL_OVERRIDE path; for outbound calls, mint the token at /voice/outgoing TwiML time, not at POST /call time.
Full context: AUDIT.md (S1). Related: #8 covered the old unauthenticated control plane (since fixed by dashboard auth); this is the remaining gap on the webhook app.
The webhook
before_requestguard validates Twilio signatures on/voice/*only (server.py:198-218) — the comment atserver.py:204-205explicitly notes the WS is unguarded. The/ws/callhandler (server.py:759-825) accepts any connection on the public port 5050, opens a Deepgram live session, and on any transcribed speech calls the LLM and TTS.Anyone who can reach port 5050 (internet-exposed by design,
README.md:58) can converse with the agent and consume Deepgram/LLM/TTS quota — completely bypassing the PIN gate, lockout, and constant-time compare (server.py:642-665). The PIN guards a door next to an open window.Fix: mint a one-time per-call token when generating the
<Connect><Stream>TwiML (server.py:653-654), pass it via<Stream><Parameter>(arrives in thestartevent'scustomParameters), and close any WS connection whosestartevent doesn't carry a valid token mapped to the expectedCallSid. Keep tokens single-use with a short TTL.Gotchas:
_ws_url()(server.py:221-227) has two call sites plus theWEBHOOK_URL_OVERRIDEpath; for outbound calls, mint the token at/voice/outgoingTwiML time, not atPOST /calltime.Full context: AUDIT.md (S1). Related: #8 covered the old unauthenticated control plane (since fixed by dashboard auth); this is the remaining gap on the webhook app.