fix(export): close stored XSS + broken neighbor links in graph.html (#1838)

The HTML report's neighbor "focus" links dropped an unescaped
JSON.stringify(nid) into a double-quoted inline onclick. The stringified
value carries its own quotes, so the attribute was truncated on every
node (links never worked), and a node id/label containing a double-quote
broke out of the attribute and injected live event handlers. AST ids are
[a-z0-9_]-safe, but ids/labels from documents or titles scraped via
`graphify add <url>` are not, so a hostile source could plant an
executable handler into a locally-opened report.

Carry the id in an HTML-escaped data-nid attribute and dispatch via one
delegated listener bound to document (survives the innerHTML rebuild that
recreates #neighbors-list). Closes the injection and repairs the links.

Reported by @edgestack-ai.

Co-Authored-By: edgestack-ai <edgestack-ai@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
safishamsi
2026-07-13 17:48:26 +01:00
co-authored by edgestack-ai Claude Opus 4.8
parent 94d3099540
commit 98c7ec039a
4 changed files with 40 additions and 2 deletions
+21
View File
@@ -159,6 +159,27 @@ def test_to_html_contains_visjs():
assert "vis-network" in content
def test_to_html_neighbor_links_have_no_inline_onclick_xss():
"""#1838: neighbor links dropped an unescaped JSON.stringify(nid) into a
quoted inline onclick — which broke every link (the value's own quotes
truncated the attribute) and let a node id/label containing a double-quote
(from a document or a scraped `graphify add` URL) inject a live event handler
into the local report (stored XSS). The template must instead carry the id in
an escaped data attribute and dispatch via one delegated listener."""
G = make_graph()
communities = cluster(G)
with tempfile.TemporaryDirectory() as tmp:
out = Path(tmp) / "graph.html"
to_html(G, communities, str(out))
html = out.read_text()
# The vulnerable inline handler is gone entirely...
assert 'onclick="focusNode(' not in html
assert "JSON.stringify(nid)" not in html
# ...replaced by an escaped data attribute + a single delegated listener.
assert 'data-nid="${esc(nid)}"' in html
assert "closest('.neighbor-link')" in html
def test_to_html_pins_visjs_version_with_sri():
"""vis-network script tag must use a pinned versioned URL with a sha384
Subresource Integrity hash and crossorigin=anonymous. Without this,