Skip to content

[Bug]: graph.html renders nothing when a label contains an unclosed <!-- followed by <script #4124

Description

@tricode-online-admin

Pre-flight checks

  • I have checked the Troubleshooting section in the README

What happened?

graphify update . writes a graph.html that renders nothing — an empty page, no graph, no legend — whenever the node labels contain an unclosed <!-- followed, later in the embedded JSON, by <script.

Expected: the page opens and draws the graph, whatever text the labels happen to carry.

Actual: the document is empty and the browser console reports SyntaxError: Unexpected token '<' in graph.html. An HTML parser finds only one inline <script> element instead of two: everything after the data block has been swallowed as script text.

Cause. The graph is embedded as JSON inside a <script> element, and the escaping only neutralises </:

# graphify/exporters/html.py, ~line 617
def _js_safe(obj) -> str:
    return json.dumps(obj).replace("</", "<\\/")

Inside a script element the HTML tokenizer does not care about quotes — it reacts to three sequences: <!--, <script and </script. An unclosed <!-- moves it to script data escaped, a later <script moves it to script data double escaped, and in that state the real </script> no longer closes the element (HTML spec, script data double escaped state). The rest of the document becomes part of the script, which is then a syntax error, so nothing runs and nothing renders.

A closed <!-- … --> inside a single label is harmless: the --> returns the tokenizer to the normal state. It takes an unclosed opener — which is exactly what label truncation produces — followed by a <script in a later label.

How I hit it. Under 0.8.19 this happened on an ordinary repository with no crafted input: fenced ```vue blocks in the markdown became nodes labelled code:vue (<!-- components/… the panel t) (truncated, so the comment is never closed) and code:vue (<script setup>). 0.9.76 no longer extracts fenced blocks as nodes, so that particular path is gone — but the escaping is unchanged, and a heading or a symbol name reaches the same place. The minimal repro below fails on 0.9.76 today.

Suggested fix: escape the < itself, not the </ pair. JavaScript reads \u003c as <, so labels still display <script setup> while the HTML tokenizer never sees the sequence:

def _js_safe(obj) -> str:
    return json.dumps(obj).replace("<", "\\u003c")

The same one-line helper is duplicated in graphify/tree_html.py and graphify/callflow_html.py and has the same gap.

Steps to reproduce

1. mkdir repro && cd repro

2. Create guide.md with exactly this content (the <!-- is deliberately never closed,
   and the <script heading must come after it):

# Guide

## An unclosed <!-- opener in a heading

Prose.

## Then a <script setup> heading

More prose.

3. Create hello.js so the corpus has a code file:

export function hello() {
  return "hi";
}

4. graphify update .

5. Open graphify-out/graph.html in a browser: the page is blank.
   The console shows SyntaxError: Unexpected token '<'.

Checking it without a browser (any HTML parser does):

  node -e "const {JSDOM}=require('jsdom');const d=new JSDOM(require('fs').readFileSync('graphify-out/graph.html','utf8'));console.log([...d.window.document.querySelectorAll('script')].length)"

  prints 2 on a healthy page (vis-network + the data block) and 1 here, because the
  closing </script> was not recognised.

Swapping the two headings so that <script setup> comes first, or closing the comment
as <!-- comment -->, produces a working page: the order and the unclosed opener are
what matter.

Error output or graph output

Browser console on opening graphify-out/graph.html:

  Uncaught SyntaxError: Unexpected token '<'   (graph.html)

Parsing the same file with a standards HTML parser (jsdom):

  inline <script> elements found: 1   (expected 2)
  block 1: SyntaxError: Unexpected token '<'
  body elements: 17                   (18 on a page that is not broken)

The embedded data, as written into graph.html:

  {"id": "guide_md_h2_1", "label": "An unclosed <!-- opener in a heading", ...}
  {"id": "guide_md_h2_2", "label": "Then a <script setup> heading", ...}

Both sequences reach the document verbatim; only "</" would have been escaped.

CLI output is normal and reports no problem:

  [graphify watch] Rebuilt: 12 nodes, 11 edges, 3 communities
  [graphify watch] graph.json, graph.html and GRAPH_REPORT.md updated in graphify-out

Graphify version

0.9.76

Operating System

Windows

Python Version

3.1

Installation Method

uv tool install (recommended)

Additional Environment Details

  • graphify 0.9.76, installed with uv tool install graphifyy (uv 0.11.16); the tool's interpreter is Python 3.14.5.
  • Windows 11, PowerShell and Git Bash alike.
  • No provider environment variables set: no OPENAI_API_KEY, GEMINI_API_KEY, GOOGLE_API_KEY or ANTHROPIC_API_KEY. The run is AST-only, no LLM involved, and the embedded labels come straight from the markdown headings.
  • A clean checkout reproduces it: the repro above is an empty directory with two files and nothing else — no config, no .graphifyignore, no cache.
  • First seen on 0.8.19 in a real repository, where fenced vue blocks in the docs produced the two labels by themselves; still reproducible on 0.9.76 with the minimal case.

Additional context

The escaping has been tightened once before, in a523509 ("fix(tree): escape title, header, and JSON blob in emit_html"), which mirrored _js_safe() into tree_html.py. That change covered </ only, so all three copies share the same gap today:

  • graphify/exporters/html.py (~line 617) — the one this report is about;
  • graphify/tree_html.py;
  • graphify/callflow_html.py.

It does not look exploitable: </script is already neutralised, so hostile content cannot close the element and inject markup. The effect is a graph page that silently will not render — which, with the CLI reporting success, is hard to attribute without reading the HTML.

A regression test would be cheap: generate a graph whose labels contain <!-- and <script, then assert that neither sequence appears inside the <script> block of the output (or, stronger, that an HTML parser finds both inline scripts).

I can open a PR with the one-line change in all three files plus that test, if it is welcome.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions