diff --git a/gcp/website/frontend_handlers.py b/gcp/website/frontend_handlers.py
index 49a8332627d..111964316ef 100644
--- a/gcp/website/frontend_handlers.py
+++ b/gcp/website/frontend_handlers.py
@@ -1103,7 +1103,7 @@ class ComputedHierarchy(NamedTuple):
def construct_hierarchy_string(target_bug_id: str, hierarchy: ComputedHierarchy,
- known_ids: set[str]) -> str:
+ known_ids: set[str]) -> Markup:
"""Constructs a hierarchy string for display.
Args:
@@ -1112,9 +1112,12 @@ def construct_hierarchy_string(target_bug_id: str, hierarchy: ComputedHierarchy,
known_ids: A set of bug IDs that are known to exist (for link generation).
Returns:
- A string representing the hierarchy for display by the frontend.
+ A Markup string representing the hierarchy for display by the frontend.
+ Vulnerability IDs are HTML-escaped so that values originating from
+ source records cannot inject markup when the template renders the
+ result.
"""
- output_lines = []
+ output_lines: list[Markup] = []
root_nodes = hierarchy.root_nodes
graph = hierarchy.graph
@@ -1126,28 +1129,29 @@ def print_subtree(vuln_id: str) -> None:
vuln_id (str): The starting vuln_id for printing the subtree.
"""
if vuln_id != target_bug_id:
+ escaped_id = escape(vuln_id)
if vuln_id in known_ids:
- output_lines.append("
" +
- vuln_id + " ")
+ output_lines.append(
+ Markup('{0} ').format(
+ escaped_id))
else:
- output_lines.append("" + vuln_id + "")
+ output_lines.append(Markup('{0}').format(escaped_id))
if vuln_id in graph:
sorted_children = sorted(graph[vuln_id])
for child in sorted_children:
if child != target_bug_id:
- output_lines.append("")
+ output_lines.append(Markup(''))
print_subtree(child)
- output_lines.append("
")
+ output_lines.append(Markup('
'))
sorted_root_nodes = sorted(root_nodes)
for root in sorted_root_nodes:
- output_lines.append("")
+ output_lines.append(Markup(''))
print_subtree(root)
- output_lines.append("
")
+ output_lines.append(Markup('
'))
- final_string = "".join(output_lines)
- return final_string
+ return Markup('').join(output_lines)
def reverse_tree(graph: dict[str, set[str]]) -> dict[str, set[str]]:
diff --git a/gcp/website/frontend_handlers_test.py b/gcp/website/frontend_handlers_test.py
index 2914690ec42..a77fa972cfb 100644
--- a/gcp/website/frontend_handlers_test.py
+++ b/gcp/website/frontend_handlers_test.py
@@ -157,6 +157,43 @@ def test_escapes_xss(self):
self.assertIn('<script>', result)
+class ConstructHierarchyStringTest(unittest.TestCase):
+ """Tests for frontend_handlers.construct_hierarchy_string."""
+
+ def _hierarchy(self, roots, graph):
+ return frontend_handlers.ComputedHierarchy(
+ root_nodes=set(roots), graph={
+ k: set(v) for k, v in graph.items()
+ })
+
+ def test_escapes_known_id(self):
+ """IDs that match known_ids must be HTML-escaped inside the tag."""
+ payload = ''
+ hierarchy = self._hierarchy([payload], {})
+ result = frontend_handlers.construct_hierarchy_string(
+ 'CVE-TARGET', hierarchy, {payload})
+ self.assertNotIn('