Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions detect/detect.go
Original file line number Diff line number Diff line change
Expand Up @@ -889,12 +889,27 @@ func (e *Engine) safeReadFileLimit(file string, limit int64) ([]byte, error) {
return nil, err
}
if info.Mode()&os.ModeSymlink != 0 {
target, err := filepath.EvalSymlinks(path)
absPath, err := filepath.Abs(path)
if err != nil {
return nil, err
}
absRoot, _ := filepath.Abs(e.Root)
if !strings.HasPrefix(target, absRoot+string(filepath.Separator)) {
target, err := filepath.EvalSymlinks(absPath)
if err != nil {
return nil, err
}
absRoot, err := filepath.Abs(e.Root)
if err != nil {
return nil, err
}
resolvedRoot, err := filepath.EvalSymlinks(absRoot)
if err != nil {
return nil, err
}
rel, err := filepath.Rel(resolvedRoot, target)
if err != nil {
return nil, err
}
if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) || filepath.IsAbs(rel) {
return nil, fmt.Errorf("symlink escapes project root: %s -> %s", file, target)
}
targetInfo, err := os.Stat(target)
Expand Down
83 changes: 83 additions & 0 deletions detect/detect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1986,6 +1986,89 @@ func languageNames(r *brief.Report) []string {
return names
}

func TestSafeReadFileSymlinks(t *testing.T) {
t.Run("internal target with symlinked root", func(t *testing.T) {
parent := t.TempDir()
realRoot := filepath.Join(parent, "project")
if err := os.Mkdir(realRoot, 0o755); err != nil {
t.Fatal(err)
}
writeProjectFile(t, realRoot, "real_go.mod", "module example.com/internal\n")

linkedRoot := filepath.Join(parent, "linked-project")
if err := os.Symlink(realRoot, linkedRoot); err != nil {
t.Skipf("creating root symlink: %v", err)
}
if err := os.Symlink("real_go.mod", filepath.Join(realRoot, "go.mod")); err != nil {
t.Skipf("creating file symlink: %v", err)
}

data, err := New(nil, linkedRoot).safeReadFile("go.mod")
if err != nil {
t.Fatalf("safeReadFile: %v", err)
}
if got, want := string(data), "module example.com/internal\n"; got != want {
t.Errorf("safeReadFile returned %q, want %q", got, want)
}
})

t.Run("internal target with relative symlinked root", func(t *testing.T) {
parent := t.TempDir()
realRoot := filepath.Join(parent, "project")
if err := os.Mkdir(realRoot, 0o755); err != nil {
t.Fatal(err)
}
writeProjectFile(t, realRoot, "real_go.mod", "module example.com/relative\n")
if err := os.Symlink("real_go.mod", filepath.Join(realRoot, "go.mod")); err != nil {
t.Skipf("creating file symlink: %v", err)
}

linkedRoot := filepath.Join(parent, "linked-project")
if err := os.Symlink("project", linkedRoot); err != nil {
t.Skipf("creating root symlink: %v", err)
}
t.Chdir(parent)

data, err := New(nil, "linked-project").safeReadFile("go.mod")
if err != nil {
t.Fatalf("safeReadFile: %v", err)
}
if got, want := string(data), "module example.com/relative\n"; got != want {
t.Errorf("safeReadFile returned %q, want %q", got, want)
}
})

t.Run("external target", func(t *testing.T) {
root := t.TempDir()
externalRoot := t.TempDir()
writeProjectFile(t, externalRoot, "external.mod", "module example.com/external\n")
if err := os.Symlink(filepath.Join(externalRoot, "external.mod"), filepath.Join(root, "go.mod")); err != nil {
t.Skipf("creating file symlink: %v", err)
}

data, err := New(nil, root).safeReadFile("go.mod")
if err == nil || !strings.Contains(err.Error(), "symlink escapes project root") {
t.Fatalf("safeReadFile error = %v, want symlink escape error", err)
}
if data != nil {
t.Errorf("safeReadFile returned %q for external target, want no data", data)
}
})

t.Run("regular file", func(t *testing.T) {
root := t.TempDir()
writeProjectFile(t, root, "go.mod", "module example.com/regular\n")

data, err := New(nil, root).safeReadFile("go.mod")
if err != nil {
t.Fatalf("safeReadFile: %v", err)
}
if got, want := string(data), "module example.com/regular\n"; got != want {
t.Errorf("safeReadFile returned %q, want %q", got, want)
}
})
}

func writeProjectFile(t *testing.T, dir, path, content string) {
t.Helper()
full := filepath.Join(dir, path)
Expand Down