Files
graphify/tests/test_html_script_breakout.py
T
safishamsiandClaude Opus 4.8 177d2071e5 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>
2026-10-10 21:20:29 +01:00

72 lines
2.7 KiB
Python

"""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