mirror of
https://github.com/lukaszraczylo/filepuff-mcp.git
synced 2026-10-18 05:48:00 +00:00
fix(search): drop matches outside the workspace root (symlink escape)
file_search runs ripgrep with --follow on by default, so a symlink inside the workspace pointing outside it (e.g. link -> /etc) caused rg to traverse out and return matches from external files — leaking their content to the caller despite the workspace-scoping contract. parseOutput emitted match paths verbatim with no re-validation. Filter every match through IsPathAllowed (resolving rg's workspace-relative path against the root first); IsPathAllowed's EvalSymlinks rejects the escape while still permitting legitimate worktree siblings. Defense in depth that also covers --no-ignore and any other traversal that reaches outside the root. Adds a security regression test (proven to fail without the filter).
This commit is contained in:
@@ -258,6 +258,17 @@ func (s *Searcher) validatePaths(paths []string) error {
|
||||
WithRemediation("Provide paths within the workspace root")
|
||||
}
|
||||
|
||||
// resultPathAllowed reports whether a ripgrep result path is within the
|
||||
// workspace root. ripgrep runs with Dir=WorkspaceRoot and emits paths relative
|
||||
// to it, so a relative path is joined to the root before the check; IsPathAllowed
|
||||
// then resolves symlinks, rejecting any path that --follow led outside the root.
|
||||
func (s *Searcher) resultPathAllowed(p string) bool {
|
||||
if !filepath.IsAbs(p) {
|
||||
p = filepath.Join(s.cfg.WorkspaceRoot, p)
|
||||
}
|
||||
return s.cfg.IsPathAllowed(p)
|
||||
}
|
||||
|
||||
// parseOutput parses ripgrep JSON output.
|
||||
func (s *Searcher) parseOutput(output *bytes.Buffer, maxResults int) (*SearchResults, error) {
|
||||
results := &SearchResults{
|
||||
@@ -288,6 +299,14 @@ func (s *Searcher) parseOutput(output *bytes.Buffer, maxResults int) (*SearchRes
|
||||
continue
|
||||
}
|
||||
|
||||
// Defense in depth: never surface a file outside the workspace root.
|
||||
// ripgrep with --follow (default on) can traverse a symlink whose
|
||||
// target lives outside the root; dropping those matches keeps search
|
||||
// from leaking external file content.
|
||||
if !s.resultPathAllowed(match.Path.Text) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check max results
|
||||
if maxResults > 0 && len(results.Results) >= maxResults {
|
||||
results.Truncated = true
|
||||
|
||||
Reference in New Issue
Block a user