diff --git a/Lib/profiling/sampling/_flamegraph_assets/flamegraph.js b/Lib/profiling/sampling/_flamegraph_assets/flamegraph.js index 840acf2c27d1201..68a2418fb9e3124 100644 --- a/Lib/profiling/sampling/_flamegraph_assets/flamegraph.js +++ b/Lib/profiling/sampling/_flamegraph_assets/flamegraph.js @@ -322,7 +322,7 @@ function createPythonTooltip(data) { const childCount = d.children ? d.children.length : 0; const source = d.data.source; - const funcname = resolveString(d.data.funcname) || resolveString(d.data.name); + const funcname = escapeHtml(resolveString(d.data.funcname) || resolveString(d.data.name) || ""); const filename = resolveString(d.data.filename) || ""; const moduleName = resolveString(d.data.module) || ""; const displayName = escapeHtml(useModuleNames ? (moduleName || filename) : filename); diff --git a/Lib/profiling/sampling/stack_collector.py b/Lib/profiling/sampling/stack_collector.py index eb1a3fba93cf33b..fc0ed1e9130fed7 100644 --- a/Lib/profiling/sampling/stack_collector.py +++ b/Lib/profiling/sampling/stack_collector.py @@ -401,7 +401,10 @@ def _get_source_lines(self, func): return None def _create_flamegraph_html(self, data): - data_json = json.dumps(data) + data_json = (json.dumps(data) + .replace("<", "\\u003c") + .replace(">", "\\u003e") + .replace("&", "\\u0026")) template_dir = importlib.resources.files(__package__) vendor_dir = template_dir / "_vendor" diff --git a/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py b/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py index 7746811014a9e2f..4b08484464bda37 100644 --- a/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py +++ b/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py @@ -570,6 +570,38 @@ def test_flamegraph_collector_export(self): self.assertIn('"value":', content) self.assertIn('"children":', content) + def test_flamegraph_escapes_special_chars_in_funcname(self): + """Function names with <, >, & must be escaped in the HTML.""" + flamegraph_out = tempfile.NamedTemporaryFile( + suffix=".html", delete=False + ) + self.addCleanup(close_and_unlink, flamegraph_out) + + collector = FlamegraphCollector(1000) + frames = [ + MockInterpreterInfo( + 0, + [MockThreadInfo( + 1, + [MockFrameInfo("test.py", 1, '')] + )], + ) + ] + collector.collect(frames) + + with captured_stdout(), captured_stderr(): + collector.export(flamegraph_out.name) + + with open(flamegraph_out.name, "r", encoding="utf-8") as f: + content = f.read() + + # The raw must not appear unescaped in the output + # (it would break out of the inline ", after_embed.split("")[0]) + def test_flamegraph_collector_empty_export_fails(self): """Test empty flamegraph export reports no output.""" flamegraph_out = tempfile.NamedTemporaryFile( diff --git a/Misc/NEWS.d/next/Library/2026-07-17-14-00-00.gh-issue-153840.FgXs3e.rst b/Misc/NEWS.d/next/Library/2026-07-17-14-00-00.gh-issue-153840.FgXs3e.rst new file mode 100644 index 000000000000000..43316035d9b2c27 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-17-14-00-00.gh-issue-153840.FgXs3e.rst @@ -0,0 +1,3 @@ +Escape special characters in ``profiling.sampling`` flamegraph function +names to prevent markup injection in the generated HTML report. Patch by +tonghuaroot.