From 4188a58ac14e868d555c5d1bbd6d9a2e81a1c81b Mon Sep 17 00:00:00 2001 From: TristanInSec Date: Fri, 10 Apr 2026 17:39:47 -0400 Subject: [PATCH 1/2] fix(website): escape vulnerability IDs in hierarchy display construct_hierarchy_string() in gcp/website/frontend_handlers.py builds the upstream/downstream hierarchy HTML for vulnerability pages by concatenating raw vuln_id strings into
  • and fragments. The result is rendered with Jinja2's |safe filter, so any non-ASCII markup characters that reach this code path would be emitted verbatim. Wrap each id with markupsafe.escape and assemble the output as markupsafe.Markup so that ids drawn from source records are always HTML-escaped in the rendered hierarchy, matching how the rest of the template handles data-derived text. Adds unit tests in frontend_handlers_test.py covering the known-id, unknown-id, and plain-id code paths. --- gcp/website/frontend_handlers.py | 28 ++++++++++++--------- gcp/website/frontend_handlers_test.py | 35 +++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 12 deletions(-) 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..82108d05265 100644 --- a/gcp/website/frontend_handlers_test.py +++ b/gcp/website/frontend_handlers_test.py @@ -157,6 +157,41 @@ 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('