search_text 泄露仓库外内容CVE 状态: 已申请,待分配。此发现已发布为 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 泄露仓库外内容gortex 的仓库索引器中存在一个不当链接解析缺陷(CWE-59),允许指向被索引仓库根目录之外的符号链接文件条目被静默遍历、内容索引,并随后由 search_text MCP 工具的实时重读接收器原样提供——包括提供给完全位于根目录之外的目标——该接收器从未应用项目自身的 guardSymlinkWithinRepo 限制检查。
zzet/gortex — https://github.com/zzet/gortex
提交 b5a63e25c0719c8ce935e59abf85618e2b4b1e64(git describe:v0.62.0-35-gb5a63e25),审计时的仓库 HEAD。确认在最新标记版本 v0.62.0 中仍然存在,即在针对无关的 GHSA-w42c-h7hr-f67p(v0.55.0)的修复落地之后。
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N — 5.5 (中危)
AV:L — 与本项目自身对其 MCP 表面的评分先例一致(GHSA-w42c-h7hr-f67p 也使用了 AV:L):默认部署是本地 MCP 守护进程,由同一主机上的代理进程通过 stdio 访问;AV:N 仅适用于可选的 --http-addr 模式。UI:R — 需要 MCP 客户端(LLM 代理,或通过 gortex call 的人类)发出 search_text / search_text regexp:true 调用。针对包含植入符号链接的已索引仓库,一个宽泛的字面量(例如单个常见字符)或匹配的正则表达式(.)就足以转储目标文件——无需对其内容有任何秘密知识——因此这是一个低门槛,但它是与单纯索引不同的独立操作。C:H — 任意文件内容泄露,不受仓库边界限制,可泄露守护进程的 OS 用户能读取的任何内容(SSH 密钥、云凭据文件、多仓库守护进程中同级租户的仓库等),并在工具响应中原样返回。I:N / A:N — 这是一个纯读取路径缺陷;没有任何内容被写入或变得不可用。internal/indexer/indexer.go:2549-2604,主要的语料库准入遍历:
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 语义,因此符号链接目录自然受到保护——对其而言 d.IsDir() 为 false,且永远不会递归进入。符号链接文件条目则没有等效保护:此处(以及 shouldExclude/shouldPruneDir,internal/indexer/indexer.go:5724-5813,它们仅匹配 gitignore 风格的路径模式)没有任何代码在将文件条目排队之前对其调用 d.Type()&os.ModeSymlink、os.Lstat 或 filepath.EvalSymlinks。一个名为例如 pwn.go 的文件,其目标为 /etc/passwd、~/.ssh/id_rsa 或同级租户仓库,仅因其名称匹配已注册的扩展名(effectiveLanguage,internal/parser 的 ,基于路径)而被准入。这个确切的已遍历文件列表成为 (:)。
search_text 接收器实时重读文件,没有限制保护internal/indexer/grep.go:17-64(GrepText / warmTrigramSearcher)直接从 idx.fileMtimes 的键构建三元组搜索器:
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 默认跟随符号链接(没有 O_NOFOLLOW)。这两个函数,以及 internal/mcp/tools_search_text.go:35-126(handleSearchText,调用 s.indexer.GrepText/GrepRegexp 的 MCP 处理器),都从未调用 resolveFilePath 或 guardSymlinkWithinRepo。这两个限制函数存在,并已正确接入所有其他内容服务路径——internal/mcp/tools_fileops.go(read_file、write_file/edit_file,自 v0.55.0 修复起)、internal/mcp/tools_coding.go:919()、、——但 的三元组 grep 路径是一个独立的接收器,从未获得相同的保护。在整个代码树中执行 确认在 、 或 中零引用。
这与 GHSA-w42c-h7hr-f67p 确实不同:
resolveFilePath 豁免绝对路径 + write_file/edit_file/move_inline 从未调用 guardSymlinkWithinRepo(已在 v0.55.0 中修复)。search_text,而非 read_file/get_symbol_source)通过一个独立地从未调用 guardSymlinkWithinRepo 的接收器(internal/search/trigram)提供其内容。写入路径修复未触及这两个位置中的任何一个。
这与已在 ozgurcd/gograph(GHSA-6h6w-vhgr-2hp6)和 vitali87/code-graph-rag()中确认的同类工具模式一致:一个没有符号链接保护的文件发现遍历,配对一个独立地缺乏限制保护的内容服务接收器。静态追踪(上文中的 file:line 引用)是完整的,并可通过检查复现。动态验证已尝试,但确实被此沙箱中的工具链缺口所阻断,如实记录而非掩盖:
$ 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
此代码库中的每个 tree-sitter 语言提取器(包括纯 Go 提取器,github.com/tree-sitter/tree-sitter-go 的 bindings/go/binding.go:// #cgo CFLAGS: -std=c11 -fPIC / import "C")都需要 cgo 才能编译——没有纯 Go 代码路径可以注册哪怕一种语言,因此没有可用的 C 编译器就无法构建 parser.Registry。此沙箱的 $PATH 中没有 gcc/cc/clang,也没有免密 root(sudo -l 要求交互式密码)来通过 apt-get install gcc 安装一个(apt 缓存中存在,gcc-16/kali-last-snapshot,但没有特权无法访问)。这是环境限制,而非关于 gortex 的发现。
PoC 测试本身——按照项目自身现有的测试框架模式编写(internal/mcp/tools_search_text_test.go 的 TestSearchText,以及先前公告自身 PoC 所使用的相同 newSingleRepoServer/callTool 形态)——留在 internal/mcp/poc_symlink_search_disclosure_test.go 中,供维护者在具有 C 工具链的环境中运行:
func TestPoC_SymlinkFollow_SearchTextDisclosesOutOfRepoContent(t *testing.T) {
outsideDir := t.TempDir()
secretPath := filepath.Join(outsideDir, "secret.txt")
const secretMarker = "GORTEX-POC-SECRET-OUTSIDE-REPO-9f3a1c"
os.WriteFile(secretPath, []byte("BEGIN OUTSIDE-REPO FILE\n"+secretMarker+"\nEND OUTSIDE-REPO FILE\n"), 0o600)
repoDir := t.TempDir()
os.WriteFile(filepath.Join(repoDir, "main.go"), []byte("package app\n\nfunc Hello() {}\n"), 0o644)
os.Symlink(secretPath, filepath.Join(repoDir, "pwn.go")) // symlink INSIDE repo -> OUTSIDE target
idx := indexer.New(g, reg, cfg.Index, zap.NewNop())
idx.Index(repoDir) // walk admits pwn.go, no symlink check
srv := NewServer(eng, g, idx, nil, zap.NewNop(), nil)
res := callTool(t, srv, "search_text", map[string]any{"query": secretMarker})
// asserts: res.IsError == false, exactly 1 match, Path == "pwn.go",
// Text contains secretMarker — i.e. the OUTSIDE file's real content.
}
鉴于上述源代码级追踪——每一步都以确切的函数命名,且从 filepath.WalkDir 经 trigram.Build/Grep 到 handleSearchText 的响应路径上任何地方都没有限制调用——这被呈现为静态追踪,而非动态确认,依据本审计反对伪造 PoC 证据的基本规则。
任何已索引的仓库若包含一个符号链接,其目标解析到该仓库根目录之外(由恶意贡献者植入、存在于克隆的依赖/插件仓库中,或通过守护进程随后索引的供应链 PR 引入),就允许 MCP 客户端——一次普通的 search_text 调用,或一次提示注入/对抗性调用,与 GHSA-w42c-h7hr-f67p 相同的威胁模型——读取该目标文件的原样内容,无论项目在其他方面对 read_file 和 get_symbol_source 强制执行何种限制保护。在多仓库守护进程(internal/indexer/multi.go:3525-3562,它针对每个被跟踪仓库展开相同的无保护调用)中,这还会跨越同一守护进程实例内的租户/仓库边界。
internal/indexer/indexer.go:2549)在没有 Lstat/ModeSymlink 检查的情况下准入符号链接文件条目。search_text(internal/mcp/tools_search_text.go)→ trigram.Build/Grep/GrepRegexp(internal/search/trigram/searcher.go)在没有任何 guardSymlinkWithinRepo 限制检查的情况下提供符号链接解析后的内容,与同一代码库中的所有其他读取接收器不同。在以下两个位置应用项目自身现有的 guardSymlinkWithinRepo(internal/mcp/tools_fileops.go:353)——或等效的基于 Lstat 的检查:
filepath.WalkDir 回调(internal/indexer/indexer.go:2549)中,拒绝或跳过 d.Type()&os.ModeSymlink != 0 的文件 DirEntry(镜像 Walk 已对目录提供的隐式保护),或在追加到 files 之前解析 filepath.EvalSymlinks(path) 并确认其包含在 absRoot 内。trigram.Build/Grep/GrepRegexp(internal/search/trigram/searcher.go)中,或在 handleSearchText 调用点,在返回匹配的 Text 之前应用 /// 已使用的相同 风格的真实路径包含检查。
任一修复单独即可关闭此问题;两者结合则与代码库其余部分已自我要求的限制姿态相匹配。Dostxodjayev Abdullox(GitHub:@squeeze440)
私有漏洞报告(PVR)已在 zzet/gortex 上启用并确认;适用标准的 GitHub 安全公告(GHSA)草稿公告流程,与项目先前的 GHSA-w42c-h7hr-f67p 披露一致。
DetectLanguageContentidx.fileMtimesinternal/indexer/indexer.go:3760idx.fileMtimes[idx.relKey(f.path)] = f.mtimeNanoget_symbol_sourceinternal/mcp/tools_lsp.go:264internal/mcp/tools_export.go:95search_textgrep -rn guardSymlinkWithinRepotools_search_text.gogrep.gotrigram/searcher.goGHSA-85gg-2gfq-q95mread_fileget_symbol_sourcetools_lsptools_exportguardSymlinkWithinRepo