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("')) sorted_root_nodes = sorted(root_nodes) for root in sorted_root_nodes: - output_lines.append("')) - 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('