dashboard.html:1595 interpolates vm.sid into onclick="deleteVm('…')" through escHtml(), which escapes <>& but not quotes (dashboard.html:1440-1444 — textContent→innerHTML round trip). A single quote in sid breaks out of the JS string inside the attribute.
sid comes from the Twilio webhook form (server.py:671). With VALIDATE_TWILIO_SIGNATURE=false (the documented local-dev mode), anyone who can reach port 5050 can POST a malicious RecordingSid to /voice/voicemail-complete and get JS execution in the authenticated dashboard when it renders the voicemail list. With validation on (default), exploitation requires a signed request, so likelihood is low — but the fix is trivial.
Fix: replace the inline handler with data-sid + a delegated addEventListener, and extend escHtml to escape ' and " for defense in depth. The rest of the dashboard escapes consistently — this is the one gap found.
Full context: AUDIT.md (S3).
dashboard.html:1595interpolatesvm.sidintoonclick="deleteVm('…')"throughescHtml(), which escapes<>&but not quotes (dashboard.html:1440-1444—textContent→innerHTMLround trip). A single quote insidbreaks out of the JS string inside the attribute.sidcomes from the Twilio webhook form (server.py:671). WithVALIDATE_TWILIO_SIGNATURE=false(the documented local-dev mode), anyone who can reach port 5050 can POST a maliciousRecordingSidto/voice/voicemail-completeand get JS execution in the authenticated dashboard when it renders the voicemail list. With validation on (default), exploitation requires a signed request, so likelihood is low — but the fix is trivial.Fix: replace the inline handler with
data-sid+ a delegatedaddEventListener, and extendescHtmlto escape'and"for defense in depth. The rest of the dashboard escapes consistently — this is the one gap found.Full context: AUDIT.md (S3).