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.
This commit is contained in:
Adolanium 2026-07-09 10:28:12 +03:00 committed by Teknium
parent a23d5073fb
commit 1f57ed2a53
2 changed files with 61 additions and 1 deletions

View file

@ -706,7 +706,7 @@ def _generate_messages_html(messages: List[Dict[str, Any]]) -> str:
<div class="tool-call">
<div class="tool-call-header">
{ICON_CHEVRON_RIGHT.replace('class="', 'class="chevron ')}
{ICON_WRENCH} Tool Call: {fn_name}
{ICON_WRENCH} Tool Call: {_escape_html(fn_name)}
</div>
<div class="tool-call-content">
<pre><code>{_escape_html(args)}</code></pre>

View file

@ -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 = '<img src=x onerror="alert(1)">'
html = _generate_messages_html([
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_1",
"type": "function",
"function": {"name": payload, "arguments": "{}"},
}
],
}
])
assert payload not in html
assert "&lt;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": "<b>x</b>"},
}
],
}
])
assert "&lt;b&gt;x&lt;/b&gt;" in html
assert "<b>x</b>" 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