Open-Source Security Intelligence

Know every vulnerability
before it knows you.

DevGuard continuously monitors your dependencies and alerts you when CVEs like this one affect your stack — with real-time threat intelligence built for developers.

Search

GHSA-vqrg-773h-4wxc

HighCVSS 7.1 / 10
Published Oct 27, 2026·Last modified Oct 27, 2026
Affected Components(1)
PyPI logoflexible-graphrag
0.6.0 – 0.8.2
Description

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.

Upload your SBOM

Upload your own SBOM in CycloneDX 1.6 or higher (JSON) directly here to check your vulnerabilities.

Risk Scores
Base Score
7.1

The vulnerability can be exploited over the network without needing physical access. It is difficult for an attacker to exploit this vulnerability and may require special conditions. An attacker needs basic access or low-level privileges. The attacker needs the user to perform some action, like clicking a link. The impact is confined to the system where the vulnerability exists. There is a high impact on the confidentiality of the information. There is a high impact on the integrity of the data. There is a high impact on the availability of the system.

Threat Intelligence
6.5

Exploitation attempts have been detected. Elevated vigilance and prompt remediation are advised.

EPSS
0.36%

The exploit probability is very low. The vulnerability is unlikely to be exploited in the next 30 days.

Exploit
Not available

We did not find any exploit available. Neither in GitHub repositories nor in the Exploit-Database.

Browse More

Scan your project

Continuously monitor your dependencies and get alerted when vulnerabilities like this one affect your stack.

Checkout DevGuard