From b0457e73c87e035c48cd840a81a6acb874c920d7 Mon Sep 17 00:00:00 2001 From: wuyangfan <1102042793@qq.com> Date: Sat, 30 May 2026 13:04:57 +0800 Subject: [PATCH] fix: skip rewriting files with setuid, setgid, or sticky bits Refuse to rewrite content when special mode bits are set so find-replace does not silently strip setuid/setgid/sticky permissions. Fixes #18 --- file_handling_test.go | 16 ++++++++++++++++ find_replace.go | 9 +++++++++ find_replace_test.go | 41 +++++++++++++++++++++++++++++++++++++++++ mode_bits.go | 7 +++++++ 4 files changed, 73 insertions(+) create mode 100644 mode_bits.go diff --git a/file_handling_test.go b/file_handling_test.go index 91ee538..47f7b9e 100644 --- a/file_handling_test.go +++ b/file_handling_test.go @@ -1,6 +1,7 @@ package main import ( + "os" "path/filepath" "testing" ) @@ -77,3 +78,18 @@ func TestNewFile(t *testing.T) { }) } } + +func TestHasSpecialFileModeBits(t *testing.T) { + if !hasSpecialFileModeBits(os.ModeSetuid | 0o755) { + t.Error("expected setuid bit to be detected") + } + if !hasSpecialFileModeBits(os.ModeSetgid | 0o755) { + t.Error("expected setgid bit to be detected") + } + if !hasSpecialFileModeBits(os.ModeSticky | 0o755) { + t.Error("expected sticky bit to be detected") + } + if hasSpecialFileModeBits(0o644) { + t.Error("expected plain mode to pass through") + } +} diff --git a/find_replace.go b/find_replace.go index 9bdbe79..b58db54 100644 --- a/find_replace.go +++ b/find_replace.go @@ -200,6 +200,15 @@ func (fr *findReplace) RenameFile(f *File) error { // ReplaceContents rewrites the file at f if its contents contain the find // string. Binary-looking files (where Read returns "") are skipped silently. func (fr *findReplace) ReplaceContents(f *File) error { + mode, err := f.Mode() + if err != nil { + return err + } + if hasSpecialFileModeBits(mode) { + log.Printf("Skipping rewrite of %v: setuid, setgid, or sticky bit set", f.Path) + return nil + } + content, err := f.Read() if err != nil { return err diff --git a/find_replace_test.go b/find_replace_test.go index 9a54564..9cf7745 100644 --- a/find_replace_test.go +++ b/find_replace_test.go @@ -660,6 +660,47 @@ func withWorkingDir(t *testing.T, dir string) { t.Cleanup(func() { _ = os.Chdir(prev) }) } + +func TestReplaceContentsSkipsSetuidFile(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("special mode bits not applicable") + } + + dir := t.TempDir() + path := filepath.Join(dir, "setuid.txt") + if err := os.WriteFile(path, []byte("needle"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.Chmod(path, 0o4755); err != nil { + t.Fatal(err) + } + + f, err := NewFile(path) + if err != nil { + t.Fatal(err) + } + mode, err := f.Mode() + if err != nil { + t.Fatal(err) + } + if !hasSpecialFileModeBits(mode) { + t.Skip("setuid bit not supported in this environment") + } + + fr := findReplace{find: "needle", replace: "hay"} + if err := fr.ReplaceContents(f); err != nil { + t.Fatal(err) + } + + got, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + if string(got) != "needle" { + t.Fatalf("content = %q; want unchanged %q", got, "needle") + } +} + func CloneRepoToTestDir(b *testing.B, repoUrl string) *File { b.Helper() d := newTestDir(b, "", "*") diff --git a/mode_bits.go b/mode_bits.go new file mode 100644 index 0000000..378896f --- /dev/null +++ b/mode_bits.go @@ -0,0 +1,7 @@ +package main + +import "os" + +func hasSpecialFileModeBits(mode os.FileMode) bool { + return mode&os.ModeSetuid != 0 || mode&os.ModeSetgid != 0 || mode&os.ModeSticky != 0 +}