Files
Privatae d62da8adb4
ci / personal-data (push) Successful in 4s
ci / fuzz (push) Successful in 2m44s
ci / test (push) Successful in 2m11s
bastion 0.54.0
2026-10-21 22:29:10 +00:00

113 lines
4.1 KiB
Go

package main
import (
"os"
"sort"
"strings"
)
// THE WINDOWS OPEN, DECIDED WHERE A LINUX TEST CAN SEE IT.
//
// mcpconfig_open_windows.go opens a config path with CreateFile and asks the
// HANDLE what it holds; every decision about the answer is here, with no
// build tag, so the Linux test run checks the Windows rules with fake handles
// (mcpconfig_winopen_test.go). Nothing here calls Windows.
//
// The open does not follow a reparse point at the last component
// (FILE_FLAG_OPEN_REPARSE_POINT): a symbolic link, junction or other
// reparse point at a config path is opened as itself, seen as a reparse
// point, and refused. What is checked is the handle that will be read, so
// there is no gap between a look and an open for a swap to fall into. A
// directory, a device, a pipe or anything else that is not a file on disk is
// refused the same way. A reparse point in a directory ABOVE the file is
// followed, as for any program opening the path; the file it leads to must
// then be owned by the account whose profile it is read for, or by SYSTEM or
// the Administrators group (mcpOwnerAllowed), so a link into another
// account's profile reads nothing of it.
// The Windows values the decision reads, from the SDK headers (winbase.h,
// winnt.h) as golang.org/x/sys/windows carries them: FILE_TYPE_DISK,
// FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_DEVICE, FILE_ATTRIBUTE_REPARSE_POINT.
const (
mcpWinFileTypeDisk = 0x0001
mcpWinAttrDirectory = 0x0010
mcpWinAttrDevice = 0x0040
mcpWinAttrReparse = 0x0400
)
// mcpWinAdminOwners: the owners a file in any account's profile may have
// besides that account. SYSTEM (S-1-5-18), and the Administrators group
// (S-1-5-32-544), which is the default owner of what an elevated
// administrator creates.
var mcpWinAdminOwners = map[string]bool{"S-1-5-18": true, "S-1-5-32-544": true}
// mcpWinHandleInfo is what the open read from the handle it holds.
type mcpWinHandleInfo struct {
FileType uint32 // GetFileType
Attributes uint32 // GetFileInformationByHandle's FileAttributes
Owner string // the owner SID from GetSecurityInfo; "" when unread
}
// mcpWinRegular: the handle is a file on disk that is not a directory, a
// device or a reparse point.
func mcpWinRegular(i mcpWinHandleInfo) bool {
return i.FileType == mcpWinFileTypeDisk &&
i.Attributes&(mcpWinAttrDirectory|mcpWinAttrDevice|mcpWinAttrReparse) == 0
}
// mcpWinOps are the Windows calls the open makes, as values so a test can
// stand fakes in for them. The field names are not method names anywhere in
// the package: the claims test resolves a call through a value by name.
type mcpWinOps struct {
openPath func(path string) (uintptr, error)
handleInfo func(h uintptr) (mcpWinHandleInfo, error)
closeHandle func(h uintptr)
asFile func(h uintptr, path string) *os.File
}
// mcpWinOpenWith opens path, checks the handle, and hands back the file and
// its owner SID. A handle that is refused is closed before return and never
// becomes a file.
func mcpWinOpenWith(ops mcpWinOps, path string) (*os.File, string, error) {
h, err := ops.openPath(path)
if err != nil {
return nil, "", err
}
info, err := ops.handleInfo(h)
if err != nil {
ops.closeHandle(h)
return nil, "", &os.PathError{Op: "query", Path: path, Err: err}
}
if !mcpWinRegular(info) {
ops.closeHandle(h)
return nil, "", errMCPNotRegular
}
owner := info.Owner
if owner == "" {
owner = mcpOwnerUnknown
}
return ops.asFile(h, path), owner, nil
}
// mcpWinProfileSID is the SID whose profile directory is dir, from the
// ProfileList (SID to ProfileImagePath, already expanded). mcpOwnerUnknown
// when no SID or more than one names it; only an administrator's or
// SYSTEM's files are then read there.
func mcpWinProfileSID(dir string, profiles map[string]string) string {
norm := func(p string) string {
return strings.ToLower(strings.TrimRight(strings.ReplaceAll(p, "/", `\`), `\`))
}
want := norm(dir)
var hits []string
for sid, p := range profiles {
if strings.HasPrefix(sid, "S-1-") && !strings.Contains(sid, ".") && norm(p) == want {
hits = append(hits, sid)
}
}
sort.Strings(hits)
if len(hits) != 1 {
return mcpOwnerUnknown
}
return hits[0]
}