fix: harden bot_name — crash guard, XSS escape, sanitization, tests

- Move `import html` to module top (was inside function body)
- Fix IndexError crash in /login when bot_name is empty string;
  use `or 'Hermes'` fallback instead of .get() default which
  doesn't guard against stored empty string
- Add server-side sanitization in POST /api/settings: strip + default
  empty/whitespace bot_name to 'Hermes' before persisting
- Escape _bn initial char in ui.js innerHTML (esc() consistency)
- Add maxlength=64 to #settingsBotName input field
- Add tests/test_sprint27.py: 9 tests covering API round-trip,
  empty/whitespace defaults, login page rendering, and XSS escaping
This commit is contained in:
Nathan Esquenazi
2026-04-06 15:06:16 +00:00
parent 9f3b2e113e
commit 71dd691ed0
4 changed files with 142 additions and 4 deletions

View File

@@ -2,6 +2,7 @@
Hermes Web UI -- Route handlers for GET and POST endpoints.
Extracted from server.py (Sprint 11) so server.py is a thin shell.
"""
import html as _html
import json
import os
import queue
@@ -116,8 +117,7 @@ def handle_get(handler, parsed) -> bool:
content_type='text/html; charset=utf-8')
if parsed.path == '/login':
import html as _html
_bn = _html.escape(load_settings().get('bot_name', 'Hermes'))
_bn = _html.escape(load_settings().get('bot_name') or 'Hermes')
_page = _LOGIN_PAGE_HTML.replace('{{BOT_NAME}}', _bn).replace('{{BOT_NAME_INITIAL}}', _bn[0].upper())
return t(handler, _page, content_type='text/html; charset=utf-8')
@@ -526,6 +526,8 @@ def handle_post(handler, parsed) -> bool:
# ── Settings (POST) ──
if parsed.path == '/api/settings':
if 'bot_name' in body:
body['bot_name'] = (str(body['bot_name']) or '').strip() or 'Hermes'
saved = save_settings(body)
saved.pop('password_hash', None) # never expose hash to client
return j(handler, saved)