diff --git a/safeopen_nix.go b/safeopen_nix.go index d1963ba..7dea9c9 100644 --- a/safeopen_nix.go +++ b/safeopen_nix.go @@ -31,6 +31,14 @@ func unixRelativePathDoesntTraverse(path string) bool { if path == "" { return false } + + // Strip leading slashes so an absolute-looking input is treated as relative + // to the base directory, matching the Linux implementation + // (canTraverseUnixRelPath). Without this, filepath.Clean("/../x") == "/x" + // hides the leading "../", so a traversal such as "/../x" is accepted here + // and escapes the base directory in openFileBeneath. + path = strings.TrimLeft(path, "/") + hasDots := false for p := path; p != ""; { var part string diff --git a/safeopen_nix_test.go b/safeopen_nix_test.go index 2e57876..1726faf 100644 --- a/safeopen_nix_test.go +++ b/safeopen_nix_test.go @@ -166,3 +166,47 @@ func TestUnixDirTraversal(t *testing.T) { t.Errorf("io.ReadAll() = %v, want = %v", string(actualData), fileContent) } } + +func TestUnixBeneathLeadingSlashTraversal(t *testing.T) { + // The base directory is a subdirectory; a secret file lives in its parent, + // outside the base. A leading-slash argument must not escape the base + // directory, for either reads or writes. Regression test for a missing + // strings.TrimLeft in unixRelativePathDoesntTraverse: filepath.Clean("/../x") + // == "/x" hid the leading "../", so "/../x" was accepted and escaped base. + parent := t.TempDir() + base := path.Join(parent, "base") + if err := os.Mkdir(base, 0777); err != nil { + t.Fatal(err) + } + if err := os.Mkdir(path.Join(base, "d"), 0777); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path.Join(parent, "secret.txt"), []byte("outside"), 0600); err != nil { + t.Fatal(err) + } + + escapes := []string{ + "/../secret.txt", + "/./../secret.txt", + "/d/../../secret.txt", + } + for _, file := range escapes { + if fd, err := OpenBeneath(base, file); err == nil { + fd.Close() + t.Errorf("OpenBeneath(%q, %q) = nil error; want rejection (escaped the base directory)", base, file) + } + if err := WriteFileBeneath(base, file, []byte("pwn"), 0600); err == nil { + t.Errorf("WriteFileBeneath(%q, %q) = nil error; want rejection (escaped the base directory)", base, file) + } + } + + // A legitimate leading-slash path that stays within base must still succeed. + if err := os.WriteFile(path.Join(base, "ok.txt"), []byte("in"), 0600); err != nil { + t.Fatal(err) + } + if fd, err := OpenBeneath(base, "/ok.txt"); err != nil { + t.Errorf("OpenBeneath(%q, %q) = %v; want success (in-base leading slash)", base, "/ok.txt", err) + } else { + fd.Close() + } +}