mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-14 07:59:40 +00:00
fix(export): complete the vis-network crash fix and pin the tooltip XSS boundary (#3699, #3686 follow-ups)
#3699: the Fermat-spiral seed positions referenced `i` inside RAW_NODES.map(n => ...) which has no index param, so the emitted JS threw 'ReferenceError: i is not defined' and graph.html failed to render at every graph size. Bind the index: map((n, i) => ...). #3686: the tooltip `title` sinks were correctly un-escaped (vis-network renders a string title via Popup.setText -> innerText, verified in the pinned 9.1.6 bundle: its only non-Element branch is innerText and it has zero innerHTML=<var> sinks). Documented that invariant at the sink as the #1838 stored-XSS boundary. Adds two guards to test_export.py: a vis-network version pin assertion (a bump must force re-verifying the innerText tooltip path) and a spiral-seed indexed-map guard (catches the free-`i` regression, which the substring-only export tests missed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
124c263488
commit
86d8aadd3b
2 files changed
+51
-1
No files matched your search
@@ -148,9 +148,13 @@ function esc(s) {{
|
||||
}}
|
||||
|
||||
// Build vis datasets
|
||||
const nodesDS = new vis.DataSet(RAW_NODES.map(n => ({{
|
||||
const nodesDS = new vis.DataSet(RAW_NODES.map((n, i) => ({{
|
||||
id: n.id, label: n.label, color: n.color, size: n.size,
|
||||
font: n.font, title: n.title,
|
||||
// Fermat/golden-angle spiral seed positions (#3699): spreading nodes out
|
||||
// before physics runs keeps avoidOverlap from computing near-zero-distance
|
||||
// repulsion that blows the BarnesHut recursion into a stack overflow on large
|
||||
// graphs. `i` is the map index — physics still settles small graphs identically.
|
||||
x: 30 * Math.sqrt(i) * Math.cos(i * 2.4),
|
||||
y: 30 * Math.sqrt(i) * Math.sin(i * 2.4),
|
||||
_community: n.community, _community_name: n.community_name,
|
||||
@@ -521,6 +525,15 @@ def to_html(
|
||||
"color": {"background": color, "border": color, "highlight": {"background": "#ffffff", "border": color}},
|
||||
"size": round(size, 1),
|
||||
"font": {"size": font_size, "color": "#ffffff"},
|
||||
# Tooltip `title` is a STRING, which vis-network renders via
|
||||
# Popup.setText -> `frame.innerText = t` (verified in the pinned
|
||||
# 9.1.6 bundle: the only non-Element branch is innerText, and the
|
||||
# bundle has zero `innerHTML = <var>` sinks). So raw special chars
|
||||
# are shown literally and must NOT be html-escaped here or the user
|
||||
# sees `&`/`<` in the tooltip (#3664/#3686). This is the
|
||||
# #1838 stored-XSS boundary: never pass an HTMLElement as `title`,
|
||||
# and do not change the vis-network pin below without re-checking
|
||||
# Popup rendering (the test_export version-pin guard enforces this).
|
||||
"title": label,
|
||||
"community": cid,
|
||||
"community_name": sanitize_label((community_labels or {}).get(cid, f"Community {cid}")),
|
||||
|
||||
@@ -1160,3 +1160,40 @@ console.log(bad);
|
||||
proc = subprocess.run([node, str(js)], capture_output=True, text=True, timeout=60)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
assert proc.stdout.strip() == "0", f"geometry violations: {proc.stdout.strip()}"
|
||||
|
||||
|
||||
def test_to_html_pins_vis_network_version_for_tooltip_xss_boundary():
|
||||
"""Tooltip `title` is passed to vis-network as a STRING and rendered via
|
||||
Popup.setText -> innerText (verified in the 9.1.6 bundle), so tooltips are
|
||||
intentionally NOT html-escaped (#3664/#3686). That safety rests on the pin:
|
||||
if vis-network is bumped, the innerText rendering path (the #1838 stored-XSS
|
||||
boundary) must be re-verified. This guard fails CI on a silent bump."""
|
||||
G = make_graph()
|
||||
communities = cluster(G)
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
out = Path(tmp) / "graph.html"
|
||||
to_html(G, communities, str(out))
|
||||
content = out.read_text()
|
||||
assert "vis-network@9.1.6" in content, (
|
||||
"vis-network pin changed — re-verify Popup.setText renders a string "
|
||||
"title via innerText (not innerHTML) before updating this pin, or the "
|
||||
"un-escaped tooltip (#3686) reopens the #1838 stored-XSS boundary"
|
||||
)
|
||||
|
||||
|
||||
def test_to_html_spiral_seed_uses_a_real_map_index():
|
||||
"""#3699: the Fermat-spiral seed positions reference `i`, so the node map
|
||||
MUST bind an index (`RAW_NODES.map((n, i) => ...)`). Without it the emitted
|
||||
JS throws `ReferenceError: i is not defined` and graph.html fails to render
|
||||
at every graph size. Guards against the free-`i` regression."""
|
||||
G = make_graph()
|
||||
communities = cluster(G)
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
out = Path(tmp) / "graph.html"
|
||||
to_html(G, communities, str(out))
|
||||
content = out.read_text()
|
||||
if "Math.sqrt(i)" in content: # spiral seed present
|
||||
assert "RAW_NODES.map((n, i)" in content, (
|
||||
"spiral seed references `i` but the node map has no index param "
|
||||
"-> ReferenceError: i is not defined (#3699)"
|
||||
)
|
||||
Reference in new issue
Block a user