mirror of
https://github.com/safishamsi/graphify.git
synced 2026-07-14 03:17:28 +00:00
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:
@@ -2,6 +2,10 @@
|
||||
|
||||
Full release notes with details on each version: [GitHub Releases](https://github.com/safishamsi/graphify/releases)
|
||||
|
||||
## 0.9.15 (2026-07-13)
|
||||
|
||||
- Security: fix a stored XSS and broken neighbor links in the exported `graph.html` (#1838, thanks @edgestack-ai). The report's neighbor "focus" links dropped an unescaped `JSON.stringify(nid)` into a double-quoted inline `onclick`. Because the stringified value carries its own quotes, the attribute was truncated on every node — so the links never worked — and a node `id`/`label` containing a double-quote broke out of the attribute and injected live handlers. AST node ids are `[a-z0-9_]`-safe, but ids/labels from documents or from titles scraped via `graphify add <url>` are not, so a hostile source could plant an executable handler into a report opened locally. The id is now carried in an HTML-escaped `data-nid` attribute and dispatched through a single delegated listener, closing the injection and repairing the links.
|
||||
|
||||
## 0.9.14 (2026-07-12)
|
||||
|
||||
- Fix: Visual Studio *solution folder* nodes no longer embed the absolute scan path (including the local username) in their `id` and `source_file` (#1789, thanks @fremat79). A solution folder is a virtual grouping, not a file — VS writes its name as both the display name and the "path" — but `extract_sln` resolved it to an absolute filesystem path anyway and keyed the node id off that. The CLI's id-relativization pass only remaps ids of real files in the scan set, so a virtual folder never matched and its absolute id survived into a committed `graph.json` (e.g. `id=/Users/<name>/proj/Plugins` instead of `id=plugins`). Solution folders are now detected (name == path) and keyed off the folder name only; real project files still resolve as before. (The earlier fix covered `.csproj`/`.sln` file nodes but missed the virtual folders — this completes it.)
|
||||
|
||||
@@ -175,7 +175,7 @@ function showInfo(nodeId) {{
|
||||
const neighborItems = neighborIds.map(nid => {{
|
||||
const nb = nodesDS.get(nid);
|
||||
const color = nb ? nb.color.background : '#555';
|
||||
return `<span class="neighbor-link" style="border-left-color:${{esc(color)}}" onclick="focusNode(${{JSON.stringify(nid)}})">${{esc(nb ? nb.label : nid)}}</span>`;
|
||||
return `<span class="neighbor-link" style="border-left-color:${{esc(color)}}" data-nid="${{esc(nid)}}">${{esc(nb ? nb.label : nid)}}</span>`;
|
||||
}}).join('');
|
||||
document.getElementById('info-content').innerHTML = `
|
||||
<div class="field"><b>${{esc(n.label)}}</b></div>
|
||||
@@ -193,6 +193,19 @@ function focusNode(nodeId) {{
|
||||
showInfo(nodeId);
|
||||
}}
|
||||
|
||||
// Neighbor links use a data attribute + one delegated listener rather than an
|
||||
// inline onclick. A node id/label sourced from a document or a scraped URL
|
||||
// (graphify add) can contain a double-quote; dropping the stringified id
|
||||
// unescaped into a quoted onclick both broke every link and allowed a hostile
|
||||
// source to inject an event handler into the local report (stored XSS, #1838).
|
||||
// esc() on data-nid keeps the value inside the attribute; the listener reads it
|
||||
// back verbatim. Bound to document so it survives the innerHTML rebuild that
|
||||
// recreates #neighbors-list on each showInfo().
|
||||
document.addEventListener('click', e => {{
|
||||
const el = e.target.closest('.neighbor-link');
|
||||
if (el && el.dataset.nid !== undefined) focusNode(el.dataset.nid);
|
||||
}});
|
||||
|
||||
// Track hovered node — hover detection is more reliable than click params
|
||||
let hoveredNodeId = null;
|
||||
network.on('hoverNode', params => {{
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "graphifyy"
|
||||
version = "0.9.14"
|
||||
version = "0.9.15"
|
||||
description = "AI coding assistant skill (Claude Code, CodeBuddy, Codex, OpenCode, Kilo Code, Cursor, Gemini CLI, Aider, OpenClaw, Factory Droid, Trae, Hermes, Kiro, Pi, Devin CLI, Google Antigravity) - turn any folder of code, docs, papers, images, or videos into a queryable knowledge graph"
|
||||
readme = "README.md"
|
||||
license = { file = "LICENSE" }
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user