mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-16 00:49:37 +00:00
fix(html): stop a node label from breaking out of the embedded script block (#4124)
Node data is embedded as JSON inside a <script> element, but the serializers escaped only `</`. An unclosed `<!--` in one label followed by a later `<script` drove the HTML tokenizer into script-data-double-escaped state, where the real </script> no longer closed the tag and graph.html rendered blank. Escape every `<` as < in both exporters/html.py and tree_html.py; labels still render because `<` round-trips through JSON.parse. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
93de0d4aea
commit
177d2071e5
3 files changed
+81
-5
No files matched your search
@@ -613,9 +613,13 @@ def to_html(
|
||||
n = member_counts.get(cid, len(communities.get(cid, []))) if member_counts else len(communities.get(cid, []))
|
||||
legend_data.append({"cid": cid, "color": color, "label": lbl, "count": n})
|
||||
|
||||
# Escape </script> sequences so embedded JSON cannot break out of the script tag
|
||||
# Escape every `<` as < so embedded JSON cannot break out of the <script>
|
||||
# element. Escaping only `</` is not enough: an unclosed `<!--` then a later
|
||||
# `<script` in a label drive the HTML tokenizer into script-data-double-escaped
|
||||
# state, where the real `</script>` no longer closes the tag and the page goes
|
||||
# blank (#4124). `<` round-trips through JSON.parse, so labels render unchanged.
|
||||
def _js_safe(obj) -> str:
|
||||
return json.dumps(obj).replace("</", "<\\/")
|
||||
return json.dumps(obj).replace("<", "\\u003c")
|
||||
|
||||
nodes_json = _js_safe(vis_nodes)
|
||||
edges_json = _js_safe(vis_edges)
|
||||
|
||||
@@ -568,9 +568,10 @@ 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=True, separators=(",", ":")).replace("</", "<\\/")
|
||||
# Escape every `<` as < so embedded JSON cannot break out of the <script>
|
||||
# element (escaping only `</` leaves the `<!--` + `<script` breakout open, #4124),
|
||||
# and HTML-escape values that land in <title>/<h1>.
|
||||
data_json = json.dumps(tree, ensure_ascii=True, separators=(",", ":")).replace("<", "\\u003c")
|
||||
return _HTML_TEMPLATE.format(
|
||||
title=_html.escape(title),
|
||||
header=_html.escape(header),
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
"""A node label can never break out of the embedded <script> block (#4124).
|
||||
|
||||
Node data is serialized to JSON and embedded inside a <script> element. Escaping
|
||||
only `</` is not enough: an unclosed `<!--` in one label followed by a later
|
||||
`<script` drives the HTML tokenizer into script-data-double-escaped state, where
|
||||
the real `</script>` no longer closes the tag — the rest of the page is swallowed
|
||||
and graph.html renders blank. Escaping every `<` as \\u003c closes all three
|
||||
breakout sequences (`<!--`, `<script`, `</script`) while labels still render,
|
||||
since `<` round-trips through JSON.parse.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import networkx as nx
|
||||
|
||||
from graphify.exporters.html import to_html
|
||||
from graphify.tree_html import emit_html
|
||||
|
||||
DANGEROUS = [
|
||||
"foo <!-- bar components the panel", # unclosed comment opener
|
||||
"code:vue (<script setup>)", # script opener
|
||||
"end </script> tail", # explicit closer
|
||||
]
|
||||
|
||||
|
||||
def _no_breakout(html: str) -> None:
|
||||
# No label-originated raw breakout sequence survives in the document.
|
||||
assert "<!--" not in html
|
||||
assert "<script setup" not in html
|
||||
assert "</script> tail" not in html
|
||||
# The escaped form is what carries the label through instead.
|
||||
assert "\\u003c" in html
|
||||
|
||||
|
||||
def test_to_html_labels_cannot_break_out_of_script(tmp_path) -> None:
|
||||
G = nx.Graph()
|
||||
for i, lbl in enumerate(DANGEROUS):
|
||||
G.add_node(f"n{i}", label=lbl, file_type="code", source_file=f"f{i}.py")
|
||||
G.add_edge("n0", "n1", relation="imports")
|
||||
out = tmp_path / "graph.html"
|
||||
assert to_html(G, {0: list(G.nodes)}, str(out)) is True
|
||||
html = out.read_text(encoding="utf-8")
|
||||
_no_breakout(html)
|
||||
# Exactly the page's own <script> elements survive — no label minted an extra one.
|
||||
assert html.count("<script") == html.count("</script")
|
||||
|
||||
|
||||
def test_tree_html_labels_cannot_break_out_of_script() -> None:
|
||||
tree = {
|
||||
"name": "root",
|
||||
"children": [{"name": lbl, "children": []} for lbl in DANGEROUS],
|
||||
}
|
||||
html = emit_html(tree, title="t", header="h")
|
||||
_no_breakout(html)
|
||||
|
||||
|
||||
def test_clean_labels_round_trip_unchanged(tmp_path) -> None:
|
||||
"""A label with no `<` is untouched, and a `<`-bearing label still decodes
|
||||
back to the original text."""
|
||||
G = nx.Graph()
|
||||
G.add_node("a", label="plain_label", file_type="code", source_file="a.py")
|
||||
G.add_node("b", label="Vector<T>", file_type="code", source_file="b.ts")
|
||||
G.add_edge("a", "b", relation="imports")
|
||||
out = tmp_path / "graph.html"
|
||||
to_html(G, {0: ["a", "b"]}, str(out))
|
||||
html = out.read_text(encoding="utf-8")
|
||||
assert "plain_label" in html
|
||||
# the escaped generic type decodes back to the original
|
||||
assert json.loads('"Vector\\u003cT>"') == "Vector<T>"
|
||||
assert "Vector\\u003cT>" in html
|
||||
Reference in new issue
Block a user