From a52350940a1d1aae8a0b723a3d275005ceefadc6 Mon Sep 17 00:00:00 2001 From: dsremo Date: Mon, 27 Apr 2026 11:55:21 +0530 Subject: [PATCH] fix(tree): escape title, header, and JSON blob in emit_html html.escape() the values that land in and <h1>, and replace </ with <\/ in the JSON embedded inside <script> so crafted graph labels or --label values cannot break out. Mirrors the _js_safe() pattern in export.py. Reported by Qodo on PR #557. --- CHANGELOG.md | 4 ++++ graphify/tree_html.py | 10 +++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac05a152f..ac305a4cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ Full release notes with details on each version: [GitHub Releases](https://githu `--label NAME`. - Implementation: `graphify/tree_html.py` (575 LOC, no external runtime dependencies — D3 v7 is loaded from cdn.jsdelivr.net). +- Security: `emit_html()` now `html.escape()`s the page title and header, + and JS-escapes `</` sequences in the embedded JSON blob so crafted + graph labels or `--label` values cannot break out of `<title>`, `<h1>`, + or the `<script>` tag (matches the `_js_safe()` pattern in `export.py`). ## 0.4.23 (2026-04-18) diff --git a/graphify/tree_html.py b/graphify/tree_html.py index 00cdfcb64..8ef177aeb 100644 --- a/graphify/tree_html.py +++ b/graphify/tree_html.py @@ -34,6 +34,7 @@ Implementation notes: from __future__ import annotations +import html as _html import json from collections import defaultdict from pathlib import Path @@ -546,12 +547,15 @@ def emit_html( svg_width: int = 6000, svg_height: int = 8000, ) -> str: + # Escape </script> sequences so embedded JSON cannot break out of the + # <script> tag, and HTML-escape values that land in <title>/<h1>. + data_json = json.dumps(tree, ensure_ascii=False, separators=(",", ":")).replace("</", "<\\/") return _HTML_TEMPLATE.format( - title=title, - header=header, + title=_html.escape(title), + header=_html.escape(header), svg_width=svg_width, svg_height=svg_height, - data_json=json.dumps(tree, ensure_ascii=False, separators=(",", ":")), + data_json=data_json, )