fix(export): escape tool-call name in HTML session export - #61345
Conversation
|
suggesting changes Security evidence:
The fix should either keep the CSP while permitting the template's own script with a nonce/hash-based Signed: GPT-5.5-xhigh in Codex |
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.
f78f79f to
301a3b6
Compare
|
Thanks, good catch. You are right that the CSP broke the multi-session export. I dropped the CSP from this PR and kept the escaping fix, which is the actual vulnerability fix. Escaping the tool-call name at the sink fully neutralizes the reported XSS, so the security goal is met without the CSP. Making the CSP work here is not a one-liner. The export ships an inline script block plus interpolated inline onclick handlers ( Changes on the updated branch:
Happy to open a follow-up that moves the inline handlers to event delegation and adds a nonce-based CSP if you want the defense in depth. |
|
fully addressed Security evidence:
The updated branch addresses the previous blocker by dropping the CSP change that broke multi-session exports while keeping the actual sink fix for the stored tool-call-name XSS. I did not find another high-confidence PR-scoped blocker. Signed: GPT-5.5-xhigh in Codex |
|
Merged — thanks @Adolanium! Clean sink fix, and good call dropping the CSP after review; the event-delegation + nonce-CSP defense-in-depth pass is a sensible follow-up. |
What does this PR do?
Fixes a stored-XSS-class escaping gap in the HTML session export.
_generate_messages_htmlinterpolates the tool-callnameinto the tool-call header (hermes_cli/session_export_html.py:709) without escaping, while every other agent-controlled field in the same renderer goes through_escape_html(arguments:712, message and tool content:720,722, reasoning:734, title:763,823, system prompt:815, model:826). The name is the only unescaped field.A tool-call name is attacker-influenced. A prompt-injected model can emit a tool call whose name is an HTML payload such as
<img src=x onerror=...>. The name does not need to be a real tool: an unknown name is refused for execution, but the raw assistant message is still persisted for agent-correction (agent/conversation_loop.py:4465), so the payload reaches the transcript and then the export viahermes_state.py:5069export_session. Opening the exported HTML in a browser runs the payload, and the template ships no Content-Security-Policy to contain it.The fix escapes the tool-call name like every sibling field, and adds a restrictive Content-Security-Policy meta tag to the export template. The CSP blocks inline and external scripts and inline event handlers, so a future unescaped sink cannot execute, while still allowing the inline styles and the Google Fonts the template already loads. Scope stays on the export renderer.
Related Issue
Fixes #61343
Type of Change
Changes Made
hermes_cli/session_export_html.py:709with_escape_html, matching every sibling field.Content-Security-Policymeta tag to the export template<head>:default-src 'none'; style-src 'unsafe-inline' https://fonts.googleapis.com; font-src https://fonts.gstatic.com. This blocks scripts and inline handlers while preserving the existing inline styles and web fonts.tests/hermes_cli/test_session_export_html.py<img onerror=...>payload is escaped in the export output.argumentsfield stays escaped.Content-Security-Policyand still permits the web fonts.How to Test
uv run --with pytest pytest tests/hermes_cli/test_session_export_html.py -q(3 new tests pass). Adjacenttests/hermes_cli/test_session_export.pyandtest_session_export_md.pystill pass (15 tests).session_export_html.pyreverted tomain, the escaping test fails because the name is emitted verbatim as<img ...>. With the fix the output contains the escaped<img ...and the CSP meta tag.<img src=x onerror=alert(1)>and open the HTML in a browser. Before the fix the handler fires. After the fix the name renders as inert text and the CSP blocks inline handlers.Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests passDocumentation & Housekeeping
docs/, docstrings) or N/Acli-config.yaml.exampleif I added/changed config keys or N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows or N/A