github.com/google/fleetspeak@v0.1.15-0.20240426164851-4f31f62c1aea/fleetspeak/src/client/socketservice/checks/sock_checks_windows.go (about)

     1  // Copyright 2017 Google Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     https://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  //go:build windows
    16  
    17  package checks
    18  
    19  import (
    20  	"encoding/base64"
    21  	"fmt"
    22  	"os"
    23  	"path/filepath"
    24  	"strings"
    25  
    26  	log "github.com/golang/glog"
    27  
    28  	"github.com/hectane/go-acl/api"
    29  	"golang.org/x/sys/windows"
    30  )
    31  
    32  // CheckSocketFile ensures the naming, filetype and ownership
    33  // (owner, group) of a Wnix socket match what we create in a Fleetspeak socket
    34  // service. This gives us some extra security. Note that using os.Lstat here
    35  // prevents confusing FS with symlink tricks.
    36  func CheckSocketFile(socketPath string) error {
    37  	if !strings.HasPrefix(socketPath[1:], `:\`) {
    38  		return fmt.Errorf(`expected a Wnix domain socket path starting with a drive letter plus ":\", got %q`, socketPath)
    39  	}
    40  
    41  	parent := filepath.Dir(socketPath)
    42  
    43  	// Lstat prevents using a symlink as the Wnix socket or the parent dir.
    44  	parentFI, err := os.Lstat(parent)
    45  	if err != nil {
    46  		return fmt.Errorf("can't stat the given socketPath's (%q) parent directory: %v", socketPath, err)
    47  	}
    48  
    49  	if !parentFI.IsDir() {
    50  		return fmt.Errorf("the given socketPath's (%q) parent directory is not a directory", socketPath)
    51  	}
    52  
    53  	// Reading file access perms on Windows is difficult, but we don't need to do
    54  	// it - if the directory is root-owned, we can assume it was created by
    55  	// Fleetspeak with the correct perms. See for example:
    56  	// https://stackoverflow.com/questions/33445727/how-to-control-file-access-in-windows
    57  	// https://msdn.microsoft.com/en-us/library/windows/desktop/aa446645.aspx
    58  
    59  	if err := checkOwnership(parent); err != nil {
    60  		log.Errorf("Unexpected ownership of socketPath's (%q) parent directory: %v", socketPath, err)
    61  	}
    62  
    63  	fi, err := os.Lstat(socketPath)
    64  	if err != nil {
    65  		return fmt.Errorf("can't stat the given socketPath (%q): %v", socketPath, err)
    66  	}
    67  
    68  	// If any bits higher than 0777 are set, this is not a regular file.
    69  	if fi.Mode() > 0777 {
    70  		return fmt.Errorf("the given socketPath (%q) is not a regular file", socketPath)
    71  	}
    72  
    73  	if err := checkOwnership(socketPath); err != nil {
    74  		log.Errorf("Unexpected ownership of socketPath (%q): %v", socketPath, err)
    75  	}
    76  
    77  	bytePipeFSPath, err := os.ReadFile(socketPath)
    78  	if err != nil {
    79  		return fmt.Errorf("os.ReadFile(%v): %v", socketPath, err)
    80  	}
    81  
    82  	return CheckSocketPipe(string(bytePipeFSPath))
    83  }
    84  
    85  // CheckSocketPipe ensures the naming and ownership (owner, group) of a Wnix
    86  // socket's underlying pipe matches what we create in a Fleetspeak socket
    87  // service. This gives us some extra security.
    88  //
    89  // Public only to support unittesting.
    90  func CheckSocketPipe(pipeFSPath string) error {
    91  	const prefix = `\\.\pipe\`
    92  	if !strings.HasPrefix(pipeFSPath, prefix) {
    93  		return fmt.Errorf(`expected a Wnix domain socket pipe path starting with %q, got %q`, prefix, pipeFSPath)
    94  	}
    95  
    96  	suffix := pipeFSPath[len(prefix):]
    97  	errSuspiciousPipe := fmt.Errorf(`the given hash-named-pipe doesn't seem to be created by Fleetspeak; got %q`, pipeFSPath)
    98  	if len(suffix) != 128 {
    99  		return errSuspiciousPipe
   100  	}
   101  	if _, err := base64.URLEncoding.Strict().DecodeString(suffix); err != nil {
   102  		return fmt.Errorf("%v\n.DecodeString(...): %v", errSuspiciousPipe, err)
   103  	}
   104  
   105  	return nil
   106  }
   107  
   108  func checkOwnership(filepath string) error {
   109  	// Owner SID of a created directory is not the same as returned by
   110  	// os/user.Current(), so we can't check against that. This can be worked
   111  	// around by creating a directory and recording what owner and group it
   112  	// gets assigned.
   113  	tmpdir, err := os.MkdirTemp("", "")
   114  	if err != nil {
   115  		return fmt.Errorf("creating tmpdir to get user SID: %w", err)
   116  	}
   117  	defer os.RemoveAll(tmpdir)
   118  
   119  	sid, groupSID, err := getOwnership(tmpdir)
   120  	if err != nil {
   121  		return fmt.Errorf("getting user/group SID: %w", err)
   122  	}
   123  
   124  	ownerSID, ownerGroupSID, err := getOwnership(filepath)
   125  	if err != nil {
   126  		return err
   127  	}
   128  
   129  	if ownerSID != sid {
   130  		return fmt.Errorf("unexpected owner SID (got %v want %v)", ownerSID, sid)
   131  	}
   132  	if ownerGroupSID != groupSID {
   133  		return fmt.Errorf("unexpected group SID (got %v want %v)", ownerGroupSID, groupSID)
   134  	}
   135  
   136  	return nil
   137  }
   138  
   139  // getOwnership returns file owner SID and file group SID.
   140  func getOwnership(filepath string) (string, string, error) {
   141  	var psidOwner, psidGroup *windows.SID
   142  	var securityDescriptor windows.Handle
   143  	err := api.GetNamedSecurityInfo(
   144  		filepath,
   145  		api.SE_FILE_OBJECT,
   146  		api.OWNER_SECURITY_INFORMATION|api.GROUP_SECURITY_INFORMATION,
   147  		&psidOwner,
   148  		&psidGroup,
   149  		nil,
   150  		nil,
   151  		&securityDescriptor,
   152  	)
   153  	if err != nil {
   154  		return "", "", err
   155  	}
   156  
   157  	// Musn't call LocalFree until we've copied the data backed by the handle (hence defer).
   158  	defer windows.LocalFree(securityDescriptor)
   159  
   160  	return psidOwner.String(), psidGroup.String(), nil
   161  }