//go:build windows package safeio import ( "errors" "os" "path/filepath" "runtime" "testing" "golang.org/x/sys/windows" ) func TestPrivateWindowsDACLRejectsPermissiveDirectoryAndRegularFile(t *testing.T) { directory := filepath.Join(t.TempDir(), "auth") if err := os.Mkdir(directory, 0o700); err != nil { t.Fatal(err) } if err := ProtectPrivateDirectory(directory); err != nil { t.Fatal(err) } if err := ValidatePrivateDirectory(directory); err != nil { t.Fatalf("ValidatePrivateDirectory() protected directory error = %v", err) } path := filepath.Join(directory, "users.yaml") if err := os.WriteFile(path, []byte("private"), 0o600); err != nil { t.Fatal(err) } if err := ProtectPrivateRegular(path); err != nil { t.Fatal(err) } if err := ValidatePrivateRegular(path); err != nil { t.Fatalf("ValidatePrivateRegular() protected file error = %v", err) } for name, path := range map[string]string{"directory": directory, "regular file": path} { t.Run(name, func(t *testing.T) { if err := setPermissiveDACL(path); err != nil { t.Fatal(err) } var err error if name == "directory" { err = ValidatePrivateDirectory(path) } else { err = ValidatePrivateRegular(path) } if !errors.Is(err, ErrUnsafeFile) { t.Fatalf("private validation error = %v, want ErrUnsafeFile", err) } }) } } func TestReplaceCanonicalRegularCreatesPrivateTemporaryAndReplacement(t *testing.T) { directory := filepath.Join(t.TempDir(), "auth") if err := os.Mkdir(directory, 0o700); err != nil { t.Fatal(err) } if err := ProtectPrivateDirectory(directory); err != nil { t.Fatal(err) } path := filepath.Join(directory, "users.yaml") if err := os.WriteFile(path, []byte("old"), 0o600); err != nil { t.Fatal(err) } if err := ProtectPrivateRegular(path); err != nil { t.Fatal(err) } temporary, err := writePrivateTemporary(directory, []byte("temporary")) if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = os.Remove(temporary) }) if err := ValidatePrivateRegular(temporary); err != nil { t.Fatalf("temporary DACL error = %v", err) } if err := ReplaceCanonicalRegular(path, []byte("replacement"), 0o600); err != nil { t.Fatal(err) } if err := ValidatePrivateRegular(path); err != nil { t.Fatalf("replacement DACL error = %v", err) } } func TestReplaceCanonicalRegularRejectsReparseParent(t *testing.T) { root := t.TempDir() realDirectory := filepath.Join(root, "real") if err := os.Mkdir(realDirectory, 0o700); err != nil { t.Fatal(err) } if err := ProtectPrivateDirectory(realDirectory); err != nil { t.Fatal(err) } target := filepath.Join(realDirectory, "users.yaml") if err := os.WriteFile(target, []byte("old"), 0o600); err != nil { t.Fatal(err) } if err := ProtectPrivateRegular(target); err != nil { t.Fatal(err) } parentLink := filepath.Join(root, "reparse-parent") if err := os.Symlink(realDirectory, parentLink); err != nil { t.Skipf("Windows host does not permit test symlink creation: %v", err) } if err := ReplaceCanonicalRegular(filepath.Join(parentLink, "users.yaml"), []byte("new"), 0o600); !errors.Is(err, ErrUnsafeFile) { t.Fatalf("reparse-parent replacement error = %v, want ErrUnsafeFile", err) } } func TestOpenCanonicalWindowsParentBlocksParentRename(t *testing.T) { root := t.TempDir() realDirectory := filepath.Join(root, "real") if err := os.Mkdir(realDirectory, 0o700); err != nil { t.Fatal(err) } if err := ProtectPrivateDirectory(realDirectory); err != nil { t.Fatal(err) } target := filepath.Join(realDirectory, "users.yaml") if err := os.WriteFile(target, []byte("old"), 0o600); err != nil { t.Fatal(err) } if err := ProtectPrivateRegular(target); err != nil { t.Fatal(err) } parents, _, err := openCanonicalWindowsParent(target) if err != nil { t.Fatal(err) } renamed := realDirectory + "-renamed" if err := os.Rename(realDirectory, renamed); err == nil { parents.Close() t.Fatal("parent rename succeeded while canonical replacement handles were retained") } parents.Close() if err := os.Rename(realDirectory, renamed); err != nil { t.Fatalf("parent rename after closing canonical replacement handles: %v", err) } } func setPermissiveDACL(path string) error { world, err := windows.StringToSid("S-1-1-0") if err != nil { return err } var pinner runtime.Pinner pinner.Pin(world) defer pinner.Unpin() acl, err := windows.ACLFromEntries([]windows.EXPLICIT_ACCESS{{ AccessPermissions: windows.GENERIC_READ | windows.GENERIC_WRITE, AccessMode: windows.GRANT_ACCESS, Trustee: windows.TRUSTEE{ TrusteeForm: windows.TRUSTEE_IS_SID, TrusteeType: windows.TRUSTEE_IS_GROUP, TrusteeValue: windows.TrusteeValueFromSID(world), }, }}, nil) if err != nil { return err } return windows.SetNamedSecurityInfo(path, windows.SE_FILE_OBJECT, windows.DACL_SECURITY_INFORMATION|windows.PROTECTED_DACL_SECURITY_INFORMATION, nil, nil, acl, nil) }