
PoC — Gortex में search_text के माध्यम से रेपो से बाहर की सामग्री के प्रकटीकरण हेतु symlink फ़ॉलोइंग (GHSA-6vhf-4wcm-2r83, CVE-2026-87003, CVSS 5.5)।
search_text के माध्यम से Out-of-Repo Content DisclosureCVE स्थिति: अनुरोध किया गया, असाइनमेंट लंबित। यह निष्कर्ष GHSA-6vhf-4wcm-2r83 के रूप में प्रकाशित किया गया है। CVE असाइनमेंट पर इस रिपॉज़िटरी का नाम बदलकर
CVE-YYYY-NNNNN-gortex-PoCकर दिया जाएगा और इस बैनर को CVE लिंक से बदल दिया जाएगा।
| शोधकर्ता | Dostxodjayev Abdullox (@squeeze440) |
| सलाह | GHSA-6vhf-4wcm-2r83 |
| CVSS 3.1 | 5.5 (मध्यम) |
| कमज़ोरी | CWE-59, CWE-200 |
search_text के माध्यम से Out-of-Repo Content Disclosuregortex के रिपॉज़िटरी indexer में एक अनुचित लिंक रिज़ॉल्यूशन दोष (CWE-59) एक symlinked file प्रविष्टि को, जो indexed रिपॉज़िटरी रूट के बाहर इंगित करती है, चुपचाप walk होने, content-indexed होने, और बाद में verbatim रूप से serve होने की अनुमति देता है — जिसमें पूरी तरह से out-of-root लक्ष्य भी शामिल है — search_text MCP टूल के live re-read sink द्वारा, जो कभी प्रोजेक्ट के स्वयं के guardSymlinkWithinRepo confinement check को लागू नहीं करता।
zzet/gortex — https://github.com/zzet/gortex
Commit b5a63e25c0719c8ce935e59abf85618e2b4b1e64 (git describe: v0.62.0-35-gb5a63e25), ऑडिट के समय रिपॉज़िटरी HEAD। नवीनतम tagged release v0.62.0 पर भी अभी भी मौजूद होने की पुष्टि की गई, अर्थात् असंबंधित GHSA-w42c-h7hr-f67p (v0.55.0) के fix के बाद भी।
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N — 5.5 (मध्यम)
AV:L — इस प्रोजेक्ट के अपने MCP सरफेस के लिए scoring precedent से मेल खाता है (GHSA-w42c-h7hr-f67p ने भी AV:L का उपयोग किया): डिफ़ॉल्ट deployment एक local MCP daemon है जिसे उसी होस्ट पर agent प्रक्रिया द्वारा stdio के माध्यम से पहुँचा जाता है; AV:N केवल opt-in --http-addr मोड पर लागू होगा।UI:R — इसके लिए MCP client (एक LLM agent, या gortex call के माध्यम से एक मानव) को search_text / search_text regexp:true कॉल जारी करने की आवश्यकता होती है। एक व्यापक literal (जैसे एक सामान्य वर्ण) या एक मेल खाता regex (.) जो planted symlink वाली indexed repo के विरुद्ध है, target फ़ाइल को dump करने के लिए पर्याप्त है — इसकी सामग्री का कोई गुप्त ज्ञान आवश्यक नहीं — इसलिए यह एक निम्न बाधा है, लेकिन यह केवल indexing से एक अलग क्रिया है।C:H — मनमानी-फ़ाइल-सामग्री का disclosure, repo सीमाओं से अपरिबद्ध, जो भी daemon का OS उपयोगकर्ता पढ़ सकता है (SSH keys, cloud credential files, multi-repo daemon में sibling tenants की repos, आदि), टूल प्रतिक्रिया में verbatim लौटाया गया।I:N / A:N — यह एक शुद्ध read-path बग है; कुछ भी लिखा या अनुपलब्ध नहीं किया जाता।internal/indexer/indexer.go:2549-2604, प्राथमिक corpus-admission walk:
err = filepath.WalkDir(absRoot, func(path string, d os.DirEntry, err error) error {
if err != nil { return nil }
if d.IsDir() {
if idx.shouldPruneDir(path, absRoot) { return filepath.SkipDir }
return nil
}
lang, ok := idx.effectiveLanguage(path, nil) // detects by extension; symlink name is enough
if !ok { return nil }
if idx.shouldExclude(path, absRoot, false) { return nil }
info, statErr := d.Info() // Lstat'd DirEntry.Info() — no ModeSymlink check
...
files = append(files, walkedFile{path: path, lang: lang, size: info.Size(), mtimeNano: info.ModTime().UnixNano()})
return nil
})
filepath.WalkDir Lstat semantics का उपयोग करता है, इसलिए एक symlinked directory स्वाभाविक रूप से सुरक्षित है — इसके लिए d.IsDir() false है और इसमें कभी recurse नहीं किया जाता। एक symlinked file प्रविष्टि को कोई समकक्ष सुरक्षा नहीं मिलती: यहाँ कुछ भी (और न ही shouldExclude/shouldPruneDir में, internal/indexer/indexer.go:5724-5813, जो केवल gitignore-style path patterns से मेल खाते हैं) फ़ाइल प्रविष्टि को queue करने से पहले d.Type()&os.ModeSymlink, os.Lstat, या filepath.EvalSymlinks को कॉल नहीं करता। pwn.go नाम की एक फ़ाइल जिसका लक्ष्य /etc/passwd, ~/.ssh/id_rsa, या एक sibling tenant repo है, केवल इसलिए स्वीकार की जाती है क्योंकि इसका नाम एक पंजीकृत extension से मेल खाता है (effectiveLanguage, internal/parser DetectLanguageContent, path-based)। यह ठीक walked-file सूची idx.fileMtimes बन जाती है (internal/indexer/indexer.go:3760: idx.fileMtimes[idx.relKey(f.path)] = f.mtimeNano)।
search_text sink बिना किसी confinement guard के live फ़ाइलों को फिर से पढ़ता हैinternal/indexer/grep.go:17-64 (GrepText / warmTrigramSearcher) सीधे idx.fileMtimes की keys से trigram searcher बनाता है:
rels := ... // from idx.fileMtimes, i.e. includes the symlinked file's rel path
idx.trigramSearcher = trigram.Build(root, rels)
internal/search/trigram/searcher.go:32-48 (Build) और :55-86/:102-176 (Grep/GrepRegexp) फिर यह करते हैं:
content, err := os.ReadFile(filepath.Join(root, filepath.FromSlash(rel))) // Build — follows symlink
...
f, err := os.Open(filepath.Join(s.root, filepath.FromSlash(rel))) // Grep/GrepRegexp — follows symlink
os.ReadFile/os.Open डिफ़ॉल्ट रूप से symlinks का अनुसरण करते हैं (कोई O_NOFOLLOW नहीं)। न तो कोई फ़ंक्शन, और न ही internal/mcp/tools_search_text.go:35-126 (handleSearchText, वह MCP handler जो s.indexer.GrepText/GrepRegexp को कॉल करता है), कभी resolveFilePath या guardSymlinkWithinRepo को कॉल करता है। वे दो confinement फ़ंक्शन मौजूद हैं और हर अन्य content-serving path में सही ढंग से wired हैं — internal/mcp/tools_fileops.go (read_file, write_file/edit_file v0.55.0 fix के अनुसार), internal/mcp/tools_coding.go:919 (get_symbol_source), internal/mcp/tools_lsp.go:264, internal/mcp/tools_export.go:95 — लेकिन search_text का trigram grep path एक अलग sink है जिसे कभी वही guard नहीं दिया गया। पूरे tree में grep -rn guardSymlinkWithinRepo tools_search_text.go, grep.go, या trigram/searcher.go में शून्य संदर्भों की पुष्टि करता है।
यह वास्तव में GHSA-w42c-h7hr-f67p से अलग है:
resolveFilePath absolute paths को छूट देता है + write_file/edit_file/move_inline कभी guardSymlinkWithinRepo को कॉल नहीं करते (v0.55.0 में fixed)।search_text, न कि read_file/get_symbol_source) अपनी सामग्री को एक sink (internal/search/trigram) के माध्यम से serve करता है जो स्वतंत्र रूप से कभी guardSymlinkWithinRepo को कॉल नहीं करता। write-path fix ने इन दोनों स्थानों में से किसी को भी नहीं छुआ।
यह sibling-tool पैटर्न से मेल खाता है जिसकी पुष्टि पहले ही ozgurcd/gograph (GHSA-6h6w-vhgr-2hp6) और vitali87/code-graph-rag (GHSA-85gg-2gfq-q95m) में की जा चुकी है: symlink सुरक्षा के बिना एक file-discovery walk, जिसके साथ एक content-serving sink जोड़ा गया है जिसमें स्वतंत्र रूप से confinement guard का अभाव है।Static trace (ऊपर file:line संदर्भ) पूर्ण है और निरीक्षण द्वारा पुनरुत्पादनीय है। Dynamic verification का प्रयास किया गया और यह इस sandbox में एक toolchain gap द्वारा वास्तव में अवरुद्ध है, जिसे छिपाने के बजाय ईमानदारी से प्रलेखित किया गया है:
$ CGO_ENABLED=1 go test ./internal/mcp/... -run TestPoC_SymlinkFollow_SearchTextDisclosesOutOfRepoContent -v
mcp [build failed]
cgo: C compiler "gcc" not found: exec: "gcc": executable file not found in $PATH