| Version | Supported |
|---|---|
| 0.10.x | Yes |
| 0.9.x | Security fixes during the 0.10 transition |
| < 0.9 | No |
Do not open a public GitHub issue for security vulnerabilities.
Report security issues via GitHub's private vulnerability reporting, or email the maintainer directly. Please include:
- Description of the vulnerability
- Steps to reproduce
- Potential impact
- Suggested fix (if any)
We will acknowledge receipt within 48 hours and aim to release a fix within 7 days for critical issues.
graphify is a local development tool. It runs as a coding-assistant skill and optionally as a local MCP stdio server. Graph analysis itself is local; configured LLM extraction backends and explicit ingest operations may make network calls.
Generating graph.html is offline. Opening the generated file normally requests
the pinned vis-network bundle from the public CDN unless it is already cached.
The artifact itself is trusted local application code: an attacker who can
replace the file can replace that code, regardless of protections applied to
the embedded graph data.
The explorer is intentionally a portable single HTML file, so its trusted
application shell contains inline CSS and JavaScript. A restrictive
Content-Security-Policy that omits unsafe-inline would therefore require
per-artifact nonces/hashes and would no longer be a simple static file that can
be copied and opened directly with file://. Graphify does not emit a
misleading permissive CSP: data/code separation, one-pass template
substitution, inert JSON embedding, text-only DOM insertion, and SRI on the one
external script are the enforced boundaries. Deployments that serve the
artifact over HTTP can add a nonce- or hash-based CSP after externalizing or
hashing the generated inline assets.
| Vector | Mitigation |
|---|---|
| SSRF via URL fetch | security.validate_url() allows only http and https schemes, blocks private/loopback/link-local IPs, and blocks cloud metadata endpoints. Redirect targets are re-validated. All fetch paths including tweet oEmbed go through safe_fetch(). |
| Oversized downloads | safe_fetch() streams responses and aborts at 50 MB. safe_fetch_text() aborts at 10 MB. |
| Non-2xx HTTP responses | safe_fetch() raises HTTPError on non-2xx status codes - error pages are not silently treated as content. |
| Path traversal in MCP server | security.validate_graph_path() resolves paths and requires them to be inside graphify-out/. Also requires the graphify-out/ directory to exist. |
| XSS in graph HTML output | sanitize_label() strips controls and caps length. The organized explorer separately JSON-escapes <, >, &, U+2028, and U+2029, stores graph data in a non-executable JSON element, and renders graph values with DOM text APIs rather than inline event attributes. |
| Script termination through graph data | The explorer escapes every < before embedding JSON, so lowercase or mixed-case </script> payloads cannot close the data element. |
| Unsafe source links | The organized explorer keeps graph-derived source values as plain text and never turns graph data into navigable links. |
| Third-party viewer dependency | vis-network is version-pinned and carries SHA-384 Subresource Integrity plus crossorigin="anonymous". The viewer fails visibly when the exact bundle cannot load. |
| Oversized graph JSON | The 512 MiB graph-file cap is checked before reads. The explorer's 5,000-node full-view threshold is a separate UI control and never bypasses this cap. |
| Prompt injection via node labels | sanitize_label() also applied to MCP text output - node labels from user-controlled source files cannot break the text format returned to agents. |
| Prompt injection via source file content | During the semantic pass, source files are attacker-controlled text mixed into the LLM context. _read_files() in llm.py wraps every file in a hash-stamped <untrusted_source path=... sha256=...> delimiter block, the extraction system prompt instructs the model to treat that block as inert data and never as instructions, and _neutralise_injection_sentinels() defangs known chat-template/jailbreak tokens (<|im_start|>, [INST], <<SYS>>, forged </untrusted_source>, etc.) before insertion. This is the table-stakes defense (issue #1210): it does not make injection impossible, but changes it from "works on first try" to "requires evasion." |
| YAML frontmatter injection | _yaml_str() escapes backslashes, double quotes, and newlines before embedding user-controlled strings (webpage titles, query questions) in YAML frontmatter. |
| Encoding crashes on source files | All tree-sitter byte slices decoded with errors="replace" - non-UTF-8 source files degrade gracefully instead of crashing extraction. |
| Symlink traversal | os.walk(..., followlinks=False) is explicit throughout detect.py. |
| Corrupted graph.json | _load_graph() in serve.py wraps json.JSONDecodeError and prints a clear recovery message instead of crashing. |
- Does not run a network listener by default (stdio transport);
--transport httpis opt-in, documented in the README, and binds to127.0.0.1unless--host 0.0.0.0is passed - Does not execute code from source files (tree-sitter parses ASTs - no eval/exec)
- Does not use
shell=Truein any subprocess call - Does not store credentials or API keys
- semantic extraction: configured LLM backends send bounded, delimited source context to the selected provider
ingestsubcommand: fetches URLs explicitly provided by the user- PDF extraction: reads local files only (pypdf does not make network calls)
- watch mode: local filesystem events only (watchdog does not make network calls)
- opening
graph.html: fetches the pinned, SRI-verified vis-network bundle