From 1f57ed2a53f0ab4eec515c8a6abf44dc1e52fdaa Mon Sep 17 00:00:00 2001 From: Adolanium <94890352+Adolanium@users.noreply.github.com> Date: Thu, 9 Jul 2026 10:28:12 +0300 Subject: [PATCH] fix(export): escape tool-call name in HTML session export The HTML session export interpolated the tool-call name into the page without escaping, while every sibling field went through _escape_html. A tool-call name is attacker-influenced, so a prompt-injected model can emit a name containing HTML that executes when the export is opened in a browser. Escape the tool-call name like the other fields. --- hermes_cli/session_export_html.py | 2 +- tests/hermes_cli/test_session_export_html.py | 60 ++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 tests/hermes_cli/test_session_export_html.py diff --git a/hermes_cli/session_export_html.py b/hermes_cli/session_export_html.py index 6b3821ed03e..2489cf653a7 100644 --- a/hermes_cli/session_export_html.py +++ b/hermes_cli/session_export_html.py @@ -706,7 +706,7 @@ def _generate_messages_html(messages: List[Dict[str, Any]]) -> str:
{ICON_CHEVRON_RIGHT.replace('class="', 'class="chevron ')} - {ICON_WRENCH} Tool Call: {fn_name} + {ICON_WRENCH} Tool Call: {_escape_html(fn_name)}
{_escape_html(args)}
diff --git a/tests/hermes_cli/test_session_export_html.py b/tests/hermes_cli/test_session_export_html.py new file mode 100644 index 00000000000..cadfb6e19a1 --- /dev/null +++ b/tests/hermes_cli/test_session_export_html.py @@ -0,0 +1,60 @@ +"""Tests for the HTML session export renderer.""" + +from hermes_cli.session_export_html import ( + _generate_messages_html, + generate_multi_session_html_export, +) + + +def test_tool_call_name_is_escaped(): + """A tool-call name is attacker-influenced (a prompt-injected model can emit + an arbitrary name), so it must be HTML-escaped like every sibling field.""" + payload = '' + html = _generate_messages_html([ + { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "id": "call_1", + "type": "function", + "function": {"name": payload, "arguments": "{}"}, + } + ], + } + ]) + assert payload not in html + assert "<img src=x onerror=" in html + + +def test_tool_call_arguments_stay_escaped(): + html = _generate_messages_html([ + { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "id": "call_1", + "type": "function", + "function": {"name": "terminal", "arguments": "x"}, + } + ], + } + ]) + assert "<b>x</b>" in html + assert "x" not in html + + +def test_multi_session_export_keeps_switcher_script(): + """The multi-session export drives session switching with an inline script, + so the escaping fix must not remove or block that script.""" + sessions = [ + {"id": "aaaa1111", "title": "First", "started_at": 0, + "messages": [{"role": "user", "content": "one"}]}, + {"id": "bbbb2222", "title": "Second", "started_at": 0, + "messages": [{"role": "user", "content": "two"}]}, + ] + html = generate_multi_session_html_export(sessions) + assert "function showSession" in html + assert 'data-id="aaaa1111"' in html + assert 'id="view-bbbb2222"' in html