From 0e4da15b9df9dc983a9ee347208b02413af573b9 Mon Sep 17 00:00:00 2001 From: Ansh Bajpai Date: Thu, 9 Jul 2026 09:49:01 +0530 Subject: [PATCH] Escape user-derived strings in the HTML report (XSS) Function names and the project root path flow into the report unescaped, in three places: - the embedded JSON: a literal in a function name terminates the report's own script block at document parse time - the table rows: names and callees hit innerHTML raw - the header: root_path is substituted into the markup raw Names contain file paths, so tracing a project with a maliciously named file makes opening the report execute arbitrary script. Fix: \u003c-escape the JSON payload, HTML-escape names client-side before innerHTML, html.escape() the root path server-side. Co-Authored-By: Claude Fable 5 --- oracletrace/reporters/html.py | 16 +++++++++---- tests/test_html_reporter.py | 42 +++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/oracletrace/reporters/html.py b/oracletrace/reporters/html.py index 1daa60a..7d3acca 100644 --- a/oracletrace/reporters/html.py +++ b/oracletrace/reporters/html.py @@ -1,5 +1,6 @@ import json from datetime import datetime +from html import escape from string import Template from typing import List from ..tracer import TracerData, FunctionData @@ -15,7 +16,7 @@ def generate_html_report(data: TracerData, output_path: str) -> None: timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") html = _JSTemplate(_HTML_TEMPLATE).substitute( - root_path=metadata.root_path, + root_path=escape(metadata.root_path), total_time=f"{metadata.total_execution_time:.4f}s", total_functions=str(metadata.total_functions), timestamp=timestamp, @@ -36,7 +37,9 @@ def _serialize_functions(functions: List[FunctionData]) -> str: "avg_time": fn.avg_time * 1000, "callees": fn.callees, }) - return json.dumps(serialized) + # < keeps a literal "" in a function name from terminating + # the inline ', + total_time=1.0, + call_count=1, + avg_time=1.0, + callees=['evil.py:'], + ) + ], + ) + output = tmp_path / "report.html" + generate_html_report(data, str(output)) + html = output.read_text(encoding="utf-8") + + # a literal inside the embedded JSON would terminate the + # report's own script block at parse time + assert "" not in html + assert "" not in html + assert "<img" in html