Skip to content

XSS + broken neighbor links: unescaped JSON.stringify in onclick attribute (export.py:325) #1838

Description

@attributex-ai

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:

  1. The neighbor "focus" links are broken in every generated graph.html — clicking a neighbor evaluates the truncated expression focusNode(, which is a syntax error.
  2. 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 IDnid = "questionnaire_skill_questionnaire":

  • Parsed onclick attribute: focusNode( ← truncated, non-functional
  • Bonus junk attribute created: questionnaire_skill_questionnaire")"

Node whose id/label contains a quotenid = '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 &quot; 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions