Bug Description
The HTML session export (hermes session export --format html) renders the tool-call name field into the page without HTML-escaping, while every other agent-controlled field in the same file goes through _escape_html. A tool-call name is attacker-influenced, so a prompt-injected model can emit a tool call whose name is an HTML/JS payload. That payload is stored in the transcript and executes when the exported file is opened in a browser. The export template also ships no Content-Security-Policy, so the injected script runs unrestricted.
Steps to Reproduce
- From the repo root, render a message with a crafted tool-call name through the export renderer:
PYTHONPATH=. python - <<'PY'
from hermes_cli.session_export_html import _generate_messages_html
payload = '<img src=x onerror="alert(document.domain)">'
html = _generate_messages_html([{
"role": "assistant", "content": "",
"tool_calls": [{"id": "call_1", "type": "function",
"function": {"name": payload, "arguments": "<b>x</b>"}}],
}])
print("name unescaped:", payload in html)
print("args escaped:", "<b>x" in html)
PY
- The output shows the tool-call name is emitted as live HTML while the sibling
arguments field on the same call is escaped:
name unescaped: True
args escaped: True
- In a real session, attacker-controlled content (a fetched web page, inbound email, or tool/MCP output) drives the model to emit the tool call. Even when an unknown tool name is refused for execution, the raw name is persisted to the transcript for agent-correction, so it reaches the export. Opening the exported HTML in a browser runs the payload.
Expected Behavior
The tool-call name is HTML-escaped like every other field, so an export opened in a browser is inert.
Actual Behavior
The tool-call name is written into HTML body context verbatim. A name such as <img src=x onerror=...> executes on open and can read and exfiltrate the export contents (which may bundle multiple sessions, system prompts, and full tool input and output). There is no Content-Security-Policy to contain it.
Affected Component
CLI (session export)
Root Cause Analysis
hermes_cli/session_export_html.py:709 interpolates {fn_name} into the tool-call header with no escaping. fn_name is read at :703 from tc.get("function", {}).get("name").
- Every sibling field is escaped, which shows the intended treatment:
arguments at :712, message and tool content at :720,722, reasoning at :734, title at :763,823, system prompt at :815, model at :826.
- The export
<head> (:26) has no Content-Security-Policy.
- The raw name reaches the transcript through
agent/conversation_loop.py:4465 even when the tool name is invalid, then the export reads it verbatim via hermes_state.py:5069 export_session.
Proposed Fix
Escape the tool-call name at session_export_html.py:709 with _escape_html, and add a restrictive Content-Security-Policy meta tag to the export template as defense in depth. A PR is attached.
Environment
- OS: Windows 11
- Python: 3.11.7
- Hermes: 0.18.2 (reproduced on main at current head)
Bug Description
The HTML session export (
hermes session export --format html) renders the tool-callnamefield into the page without HTML-escaping, while every other agent-controlled field in the same file goes through_escape_html. A tool-call name is attacker-influenced, so a prompt-injected model can emit a tool call whose name is an HTML/JS payload. That payload is stored in the transcript and executes when the exported file is opened in a browser. The export template also ships no Content-Security-Policy, so the injected script runs unrestricted.Steps to Reproduce
argumentsfield on the same call is escaped:Expected Behavior
The tool-call name is HTML-escaped like every other field, so an export opened in a browser is inert.
Actual Behavior
The tool-call name is written into HTML body context verbatim. A name such as
<img src=x onerror=...>executes on open and can read and exfiltrate the export contents (which may bundle multiple sessions, system prompts, and full tool input and output). There is no Content-Security-Policy to contain it.Affected Component
CLI (session export)
Root Cause Analysis
hermes_cli/session_export_html.py:709interpolates{fn_name}into the tool-call header with no escaping.fn_nameis read at:703fromtc.get("function", {}).get("name").argumentsat:712, message and tool content at:720,722, reasoning at:734, title at:763,823, system prompt at:815, model at:826.<head>(:26) has no Content-Security-Policy.agent/conversation_loop.py:4465even when the tool name is invalid, then the export reads it verbatim viahermes_state.py:5069export_session.Proposed Fix
Escape the tool-call name at
session_export_html.py:709with_escape_html, and add a restrictive Content-Security-Policy meta tag to the export template as defense in depth. A PR is attached.Environment