Skip to content

Commit ade6fdc

Browse files
committed
fix(security): skip symlinks in all filepath.Walk callbacks (M1)
filepath.Walk uses Lstat internally and does NOT descend into symlinked directories. However, symlink entries are still passed to the walk callback — appearing as files in results. Since H1 fixed all file opens to use O_NOFOLLOW, these symlink paths are unreadable dead ends that leak path information. Fix: add info.Mode()&os.ModeSymlink check at the top of every walk callback across the codebase: file_tool.go — searchContent, searchFiles name match, glob with path separators perf_tools.go — multiGrep resource.go — FileResolver.walkAndMatch All five walks now silently skip symlink entries. This is defense-in-depth: O_NOFOLLOW on opens was the primary fix, skipping symlinks at the walk level prevents path disclosure and eliminates any TOCTOU concern entirely. Fixes M1 from security audit.
1 parent 1a5099b commit ade6fdc

3 files changed

Lines changed: 23 additions & 0 deletions

File tree

cmd/odek/file_tool.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,11 @@ func (t *searchFilesTool) searchContent(args searchFilesArgs) (string, error) {
380380
}
381381
return nil
382382
}
383+
// Skip symlinks — prevents TOCTOU on the path and avoids listing
384+
// files the agent can't read (O_NOFOLLOW opens would fail anyway).
385+
if info.Mode()&os.ModeSymlink != 0 {
386+
return nil
387+
}
383388

384389
// Apply file_glob filter
385390
if args.FileGlob != "" {
@@ -474,6 +479,10 @@ func (t *searchFilesTool) searchFiles(args searchFilesArgs) (string, error) {
474479
}
475480
return nil
476481
}
482+
// Skip symlinks — prevents listing files the agent can't read.
483+
if info.Mode()&os.ModeSymlink != 0 {
484+
return nil
485+
}
477486
match, _ := filepath.Match(pattern, info.Name())
478487
if match {
479488
matches = append(matches, searchMatch{Path: path})
@@ -1034,6 +1043,11 @@ func (t *globTool) Call(argsJSON string) (result string, err error) {
10341043
if err != nil || info == nil {
10351044
return nil
10361045
}
1046+
// Skip symlinks — prevents traversal via symlinked directories
1047+
// and avoids listing files the agent can't read.
1048+
if info.Mode()&os.ModeSymlink != 0 {
1049+
return nil
1050+
}
10371051
match, _ := filepath.Match(args.Pattern, path)
10381052
if !match {
10391053
// Also try relative to root

cmd/odek/perf_tools.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1091,6 +1091,10 @@ func (t *multiGrepTool) searchPattern(pattern, root, fileGlob string, limit int)
10911091
}
10921092
return nil
10931093
}
1094+
// Skip symlinks — prevents TOCTOU and listing unreadable files.
1095+
if info.Mode()&os.ModeSymlink != 0 {
1096+
return nil
1097+
}
10941098
if fileGlob != "" {
10951099
match, _ := filepath.Match(fileGlob, info.Name())
10961100
if !match {

internal/resource/resource.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,11 @@ func (f *FileResolver) walkAndMatch(searchTerm string) []string {
289289
if err != nil {
290290
return nil
291291
}
292+
// Skip symlinks — resource resolver uses O_NOFOLLOW on Load,
293+
// so symlinks are unreadable anyway.
294+
if info.Mode()&os.ModeSymlink != 0 {
295+
return nil
296+
}
292297
if info.IsDir() {
293298
if skipDir(info.Name()) {
294299
return filepath.SkipDir

0 commit comments

Comments
 (0)