
PoC — Symlink-Verfolgung zur Offenlegung von Inhalten außerhalb des Repos über search_text in Gortex (GHSA-6vhf-4wcm-2r83, CVE-2026-87003, CVSS 5.5).
search_textCVE-Status: beantragt, Zuweisung ausstehend. Dieser Befund wird als GHSA-6vhf-4wcm-2r83 veröffentlicht. Bei CVE-Zuweisung wird dieses Repository in
CVE-YYYY-NNNNN-gortex-PoCumbenannt und dieses Banner durch den CVE-Link ersetzt.
| Forscher | Dostxodjayev Abdullox (@squeeze440) |
| Advisory | GHSA-6vhf-4wcm-2r83 |
| CVSS 3.1 | 5.5 (Mittel) |
| Schwäche | CWE-59, CWE-200 |
search_textEin Fehler bei der unsachgemäßen Link-Auflösung (CWE-59) im Repository-Indexer von gortex ermöglicht es, dass ein symlinkter Datei-Eintrag, der auf ein Ziel außerhalb des indizierten Repository-Roots zeigt, stillschweigend durchlaufen, in den Index aufgenommen und später wörtlich ausgeliefert wird — einschließlich an ein vollständig außerhalb des Roots liegendes Ziel — durch die Live-Re-Read-Senke des MCP-Tools search_text, die niemals die eigene guardSymlinkWithinRepo-Eingrenzungsprüfung des Projekts anwendet.
zzet/gortex — https://github.com/zzet/gortex
Commit b5a63e25c0719c8ce935e59abf85618e2b4b1e64 (git describe: v0.62.0-35-gb5a63e25), der Repository-HEAD zum Zeitpunkt des Audits. Bestätigt weiterhin vorhanden im neuesten getaggten Release v0.62.0, d. h. nachdem der Fix für das unabhängige GHSA-w42c-h7hr-f67p (v0.55.0) eingeflossen ist.
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N — 5.5 (Mittel)
AV:L — entspricht dem eigenen Scoring-Präzedenzfall dieses Projekts für seine MCP-Oberfläche (GHSA-w42c-h7hr-f67p verwendete ebenfalls AV:L): Die Standardbereitstellung ist ein lokaler MCP-Daemon, der über stdio vom Agent-Prozess auf demselben Host erreicht wird; AV:N würde nur für den optionalen --http-addr-Modus gelten.UI:R — erfordert, dass der MCP-Client (ein LLM-Agent oder ein Mensch über gortex call) einen search_text- / search_text regexp:true-Aufruf ausführt. Ein breites Literal (z. B. ein einzelnes häufiges Zeichen) oder eine passende Regex (.) gegen ein indiziertes Repo, das den platzierten Symlink enthält, reicht aus, um die Zieldatei auszugeben — kein geheimes Wissen über deren Inhalt erforderlich —, also eine niedrige Hürde, aber dennoch eine eigenständige Aktion gegenüber dem bloßen Indexieren.C:H — Offenlegung beliebiger Dateiinhalte, unbegrenzt durch Repo-Grenzen, von allem, was der OS-Benutzer des Daemons lesen kann (SSH-Schlüssel, Cloud-Credential-Dateien, Repos benachbarter Tenants in einem Multi-Repo-Daemon usw.), wörtlich in der Tool-Antwort zurückgegeben.I:N / A:N — dies ist ein reiner Lesepfad-Fehler; nichts wird geschrieben oder unverfügbar gemacht.internal/indexer/indexer.go:2549-2604, der primäre Korpus-Aufnahme-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 verwendet Lstat-Semantik, sodass ein symlinktes Verzeichnis natürlicherweise geschützt ist — d.IsDir() ist dafür false und es wird nie rekursiv betreten. Ein symlinkter Datei-Eintrag erhält keinen äquivalenten Schutz: Nichts hier (noch in shouldExclude/shouldPruneDir, internal/indexer/indexer.go:5724-5813, die nur gitignore-artige Pfadmuster abgleichen) ruft d.Type()&os.ModeSymlink, os.Lstat oder filepath.EvalSymlinks für den Datei-Eintrag auf, bevor er in die Warteschlange gestellt wird. Eine Datei namens z. B. pwn.go, deren Ziel /etc/passwd, ~/.ssh/id_rsa oder ein Repo eines benachbarten Tenants ist, wird allein deshalb aufgenommen, weil ihr Name auf eine registrierte Erweiterung passt (effectiveLanguage, internal/parser DetectLanguageContent, pfadbasiert). Genau diese walked-file-Liste wird zu idx.fileMtimes (internal/indexer/indexer.go:3760: idx.fileMtimes[idx.relKey(f.path)] = f.mtimeNano).
search_text-Senke liest Live-Dateien ohne Eingrenzungsschutz erneutinternal/indexer/grep.go:17-64 (GrepText / warmTrigramSearcher) baut den Trigram-Searcher direkt aus den Schlüsseln von 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) und :55-86/:102-176 (Grep/GrepRegexp) tun dann:
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 folgen standardmäßig Symlinks (kein O_NOFOLLOW). Weder diese Funktionen noch internal/mcp/tools_search_text.go:35-126 (handleSearchText, der MCP-Handler, der s.indexer.GrepText/GrepRegexp aufruft) rufen jemals resolveFilePath oder guardSymlinkWithinRepo auf. Diese beiden Eingrenzungsfunktionen existieren und sind korrekt in jeden anderen inhaltsausliefernden Pfad eingebunden — internal/mcp/tools_fileops.go (read_file, write_file/edit_file seit dem 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 —, aber der Trigram-Grep-Pfad von search_text ist eine separate Senke, die nie denselben Schutz erhalten hat. grep -rn guardSymlinkWithinRepo über den gesamten Baum bestätigt null Referenzen in tools_search_text.go, grep.go oder trigram/searcher.go.
Dies ist tatsächlich eigenständig gegenüber GHSA-w42c-h7hr-f67p:
resolveFilePath nimmt absolute Pfade aus + write_file/edit_file/move_inline rufen nie guardSymlinkWithinRepo auf (behoben in v0.55.0).search_text, nicht read_file/get_symbol_source) liefert deren Inhalt über eine Senke (internal/search/trigram) aus, die unabhängig davon nie guardSymlinkWithinRepo aufruft. Der Schreibpfad-Fix hat keinen dieser beiden Orte berührt.
Dies entspricht dem bereits in ozgurcd/gograph (GHSA-6h6w-vhgr-2hp6) und vitali87/code-graph-rag (GHSA-85gg-2gfq-q95m) bestätigten Schwester-Tool-Muster: ein Datei-Discovery-Walk ohne Symlink-Schutz, gepaart mit einer inhaltsausliefernden Senke, der unabhängig davon ein Eingrenzungsschutz fehlt.Statische Spur (Datei:Zeile-Referenzen oben) ist vollständig und durch Inspektion reproduzierbar. Dynamische Verifikation wurde versucht und wird tatsächlich durch eine Toolchain-Lücke in dieser Sandbox blockiert, ehrlich dokumentiert statt übertüncht: