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 }