Summary
The HTML exporter emits an inline onclick handler by dropping JSON.stringify(nid) unescaped into a double-quoted HTML attribute. Because JSON.stringify wraps the value in its own ", the attribute is terminated early on every node. This has two consequences:
- The neighbor "focus" links are broken in every generated
graph.html — clicking a neighbor evaluates the truncated expression focusNode(, which is a syntax error.
- Stored XSS when any node
id or label contains a double-quote — the extra " breaks out of the attribute and injects arbitrary attributes/handlers. Node IDs from AST extraction are [a-z0-9_]-safe, but IDs and labels sourced from documents, or from scraped page titles via graphify add <url>, are not constrained, so a hostile source file/URL can plant an executable handler into the report a user opens locally.
Location
graphify/export.py:325 (v0.8.50):
return `<span class="neighbor-link" style="border-left-color:${{esc(color)}}" onclick="focusNode(${{JSON.stringify(nid)}})">${{esc(nb ? nb.label : nid)}}</span>`;
Every other interpolation on the adjacent lines is run through esc(...); only the onclick payload is not.
Root cause
Inside a double-quoted attribute, JSON.stringify("abc") produces the literal characters "abc". The browser parses onclick="focusNode(" and stops at the first inner quote — so the handler is always truncated, and any additional " in the value starts new attributes.
Reproduction
Running the exact line-325 template in a browser (via the DOM parser, no custom escaping):
Normal node ID — nid = "questionnaire_skill_questionnaire":
- Parsed
onclick attribute: focusNode( ← truncated, non-functional
- Bonus junk attribute created:
questionnaire_skill_questionnaire")"
Node whose id/label contains a quote — nid = 'x" onmouseover="alert(1)" data-x="':
- Injected attributes on the span:
onmouseover, data-x ← live event handler, XSS
Suggested fix
HTML-escape the stringified value so the quotes can't break out:
onclick="focusNode(${{esc(JSON.stringify(nid))}})"
That closes the injection and repairs the currently-broken links (the escaped " parses back to " inside the JS string). Cleaner still, drop the inline handler and use a delegated listener:
<span class="neighbor-link" data-nid="${{esc(nid)}}" ...>
document.getElementById('neighbors-list').addEventListener('click', e => {
const el = e.target.closest('.neighbor-link');
if (el) focusNode(el.dataset.nid);
});
Environment
graphifyy 0.8.50 (PyPI)
- Reproduced against
graph.html generated by graphify export html
Found while running graphify on a local repo; happy to send a PR for either fix if useful.
Summary
The HTML exporter emits an inline
onclickhandler by droppingJSON.stringify(nid)unescaped into a double-quoted HTML attribute. BecauseJSON.stringifywraps the value in its own", the attribute is terminated early on every node. This has two consequences:graph.html— clicking a neighbor evaluates the truncated expressionfocusNode(, which is a syntax error.idorlabelcontains a double-quote — the extra"breaks out of the attribute and injects arbitrary attributes/handlers. Node IDs from AST extraction are[a-z0-9_]-safe, but IDs and labels sourced from documents, or from scraped page titles viagraphify add <url>, are not constrained, so a hostile source file/URL can plant an executable handler into the report a user opens locally.Location
graphify/export.py:325(v0.8.50):Every other interpolation on the adjacent lines is run through
esc(...); only theonclickpayload is not.Root cause
Inside a double-quoted attribute,
JSON.stringify("abc")produces the literal characters"abc". The browser parsesonclick="focusNode("and stops at the first inner quote — so the handler is always truncated, and any additional"in the value starts new attributes.Reproduction
Running the exact line-325 template in a browser (via the DOM parser, no custom escaping):
Normal node ID —
nid = "questionnaire_skill_questionnaire":onclickattribute:focusNode(← truncated, non-functionalquestionnaire_skill_questionnaire")"Node whose id/label contains a quote —
nid = 'x" onmouseover="alert(1)" data-x="':onmouseover,data-x← live event handler, XSSSuggested fix
HTML-escape the stringified value so the quotes can't break out:
That closes the injection and repairs the currently-broken links (the escaped
"parses back to"inside the JS string). Cleaner still, drop the inline handler and use a delegated listener:Environment
graphifyy0.8.50 (PyPI)graph.htmlgenerated bygraphify export htmlFound while running graphify on a local repo; happy to send a PR for either fix if useful.