Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Lib/profiling/sampling/stack_collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,10 @@ def _create_flamegraph_html(self, data):
# Python recursion limit. iterencode() uses the Python encoder.
data_json = "".join(json.JSONEncoder().iterencode(data))

# Prevent profile strings from closing the script element or starting
# an HTML comment, while preserving their values in JavaScript.
data_json = data_json.replace("<", "\\u003c")

template_dir = importlib.resources.files(__package__)
vendor_dir = template_dir / "_vendor"
assets_dir = template_dir / "_assets"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import sys
import tempfile
import unittest
from unittest import mock

from test.support import is_emscripten, run_with_limited_c_stack, set_recursion_limit

Expand Down Expand Up @@ -596,6 +597,35 @@ def test_flamegraph_collector_export(self):
self.assertIn('"sample_interval_usec": 10000', content)
self.assertIn("samples * data.stats.sample_interval_usec / 1000", content)

def test_flamegraph_html_escapes_script_data(self):
collector = FlamegraphCollector(1000)
data = {
"strings": [
"</script><script>alert(1)</script>",
"</ScRiPt \t><script>alert(2)</script>",
"<!--<script>",
"<>&\u2028\u2029",
'quotes " and \\ and \\u003c',
"计算\udc80",
],
"stats": {},
}
for side_effect in (None, RecursionError):
with self.subTest(side_effect=side_effect):
# Also exercise the fallback used for deeply nested profiles.
with mock.patch(
"profiling.sampling.stack_collector.json.dumps",
wraps=json.dumps,
side_effect=side_effect,
):
content = collector._create_flamegraph_html(data)
embedded_data = content.split("const EMBEDDED_DATA = ", 1)[1]
embedded_data = embedded_data.split(";\n", 1)[0]
self.assertNotIn("<", embedded_data)
self.assertNotIn("\u2028", embedded_data)
self.assertNotIn("\u2029", embedded_data)
self.assertEqual(json.loads(embedded_data), data)

def test_flamegraph_collector_empty_export_fails(self):
"""Test empty flamegraph export reports no output."""
flamegraph_out = tempfile.NamedTemporaryFile(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Escape profile data embedded in sampling profiler flamegraph reports to
prevent strings containing ``</script>`` from breaking the generated HTML.
Loading