# Flexible GraphRAG: Cypher/Graph Injection via broken string-escaping in FalkorDB adapter (_escape()) - full graph wipe from ingested-document content

Broken escape ordering in Flexible GraphRAG's FalkorDB adapter lets LLM-extracted entity IDs from ingested documents inject Cypher, wiping the entire graph.

- Payout: Research
- Severity: high
- Program: Flexible GraphRAG
- Found by: KulFilip
- Disclosed: 2026-10-27
- CVE: CVE-2026-114510
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-vqrg-773h-4wxc
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-vqrg-773h-4wxc
- Tags: area:ai domain:ai domain:web3 impact:data-leak impact:integrity kind:research platform:ghsa sev:high tech:llm tech:python technique:chained technique:source-review vuln:command-injection vuln:prompt-injection

**Target:** FalkorDB adapter in langchain graph backend (pip flexible-graphrag)
**Root cause:** _escape() replaces ' before \, so a trailing quote becomes \\' which Cypher parses as escaped backslash plus a string-terminating quote; escaped values are then f-string interpolated into Cypher instead of parameterized.
**Steps:**
1. Craft a document whose text makes the extraction LLM emit an entity id of `pwn' }) MATCH (m) DETACH DELETE m RETURN count(m) AS pwn //`
2. Ingest the document so the id flows into the MERGE query template
3. Buggy _escape() outputs `pwn\\' }) MATCH (m) DETACH DELETE m RETURN count(m) AS pwn //`
4. Resulting query: `MERGE (n:`Entity` {id: 'pwn\\' }) MATCH (m) DETACH DELETE m RETURN count(m) AS pwn //'}) RETURN distinct 'done' AS result`
5. FalkorDB requires `WITH 1 AS x` between MERGE and MATCH; add it to the payload to delete every node
**How found:** Code review of the _escape() helper spotted the reversed replace order — the same double-escape class as classic HTML escaping bugs — then traced the escaped value into f-string-built Cypher queries and reasoned through the parser's backslash convention to confirm the string breaks open.
**Exploitation:** Attacker-controlled document content is extracted by the LLM into node/relationship ids, which are interpolated into unparameterized Cypher. The broken escape lets the id string terminate early, chaining a second clause that runs with the adapter's full write privileges; maintainer confirmed the WITH-adjusted payload deleted every node in a live FalkorDB graph.
**Escalation:** Same primitive adaptable to data exfiltration via RETURN of other nodes' properties or tampering with existing records, depending on what the caller surfaces.
**Insight:** When auditing escape helpers, check replace() ordering: escaping the quote before the backslash re-escapes the just-inserted backslash, so any input ending in a quote escapes itself. Also check backtick-quoted identifiers (labels/relationship types) which often get no escaping at all.

## Proof of concept (copied exactly from the original report)

**Attacker-controlled node.id payload for Cypher injection**

```
"pwn' }) MATCH (m) DETACH DELETE m RETURN count(m) AS pwn //"
```

**Reconstructed injected Cypher query 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
```

**Buggy _escape() function with reversed escaping order**

```
def _escape(v: str) -> str:
    return str(v).replace("'", "\\'").replace("\\", "\\\\")
```

