diff --git a/server/services/adminreset/transport_test.go b/server/services/adminreset/transport_test.go index 01ca5ab..4fd7191 100644 --- a/server/services/adminreset/transport_test.go +++ b/server/services/adminreset/transport_test.go @@ -91,3 +91,41 @@ func TestUnixEmptyAllowedUIDsRejectConnection(t *testing.T) { t.Fatalf("response = %+v", response) } } + +func TestUnixListenRejectsWritableControlDirectory(t *testing.T) { + parent := t.TempDir() + "/control" + if err := os.Mkdir(parent, 0700); err != nil { + t.Fatal(err) + } + if err := os.Chmod(parent, 0777); err != nil { + t.Fatal(err) + } + if _, err := ListenUnix(parent+"/control.sock", []uint32{uint32(os.Getuid())}); err == nil { + t.Fatal("writable control directory was accepted") + } +} + +func TestUnixControlSocketUsesRestrictedPermissions(t *testing.T) { + parent := t.TempDir() + "/control" + path := parent + "/control.sock" + server, err := ListenUnix(path, []uint32{uint32(os.Getuid())}) + if err != nil { + t.Fatal(err) + } + defer server.Close() + + parentInfo, err := os.Stat(parent) + if err != nil { + t.Fatal(err) + } + if parentInfo.Mode().Perm() != 0750 { + t.Fatalf("control directory permissions = %04o, want 0750", parentInfo.Mode().Perm()) + } + socketInfo, err := os.Lstat(path) + if err != nil { + t.Fatal(err) + } + if socketInfo.Mode().Perm() != 0600 { + t.Fatalf("control socket permissions = %04o, want 0600", socketInfo.Mode().Perm()) + } +} diff --git a/server/services/adminreset/transport_unix.go b/server/services/adminreset/transport_unix.go index 1c49f06..3f2f3dd 100644 --- a/server/services/adminreset/transport_unix.go +++ b/server/services/adminreset/transport_unix.go @@ -30,8 +30,8 @@ func ListenUnix(path string, allowedUIDs []uint32) (*UnixServer, error) { if err := os.MkdirAll(parent, 0750); err != nil { return nil, fmt.Errorf("create control directory: %w", err) } - if info, err := os.Lstat(parent); err != nil || info.Mode()&os.ModeSymlink != 0 || !info.IsDir() { - return nil, fmt.Errorf("control directory is not secure") + if err := validateControlDirectory(parent); err != nil { + return nil, err } if err := os.Chmod(parent, 0750); err != nil { return nil, fmt.Errorf("set control directory permissions: %w", err) @@ -40,6 +40,9 @@ func ListenUnix(path string, allowedUIDs []uint32) (*UnixServer, error) { if info.Mode()&os.ModeSymlink != 0 || info.Mode()&os.ModeSocket == 0 { return nil, fmt.Errorf("control path is not a socket") } + if err := validateControlSocketOwner(info); err != nil { + return nil, err + } if conn, dialErr := net.DialTimeout("unix", path, 100*time.Millisecond); dialErr == nil { _ = conn.Close() return nil, fmt.Errorf("control socket is already active") @@ -66,6 +69,29 @@ func ListenUnix(path string, allowedUIDs []uint32) (*UnixServer, error) { return &UnixServer{listener: listener, path: path, allowed: allowed}, nil } +func validateControlDirectory(path string) error { + info, err := os.Lstat(path) + if err != nil || info.Mode()&os.ModeSymlink != 0 || !info.IsDir() { + return fmt.Errorf("control directory is not secure") + } + if info.Mode().Perm()&0o022 != 0 { + return fmt.Errorf("control directory must not be writable by group or other users") + } + return validateControlOwner(info, "control directory") +} + +func validateControlSocketOwner(info os.FileInfo) error { + return validateControlOwner(info, "control socket") +} + +func validateControlOwner(info os.FileInfo, resource string) error { + stat, ok := info.Sys().(*syscall.Stat_t) + if !ok || stat.Uid != uint32(os.Geteuid()) { + return fmt.Errorf("%s owner does not match service identity", resource) + } + return nil +} + func (s *UnixServer) Serve(ctx context.Context, handler ResetHandler, guard *SessionGuard) error { if s == nil || s.listener == nil || handler == nil || guard == nil { return ErrInvalidRequest