143 lines
4.5 KiB
Go
143 lines
4.5 KiB
Go
//go:build windows
|
|
|
|
package safeio
|
|
|
|
import (
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
const windowsRetainedHandleShareMode uint32 = windows.FILE_SHARE_READ | windows.FILE_SHARE_WRITE
|
|
|
|
// ReadCanonicalRegular opens each component with FILE_FLAG_OPEN_REPARSE_POINT and rejects a
|
|
// reparse point on the opened handle before opening the next component. Retained handles allow
|
|
// ordinary read/write sharing but deny delete sharing, which blocks rename or deletion after a
|
|
// component is opened and throughout the final read. Windows' Win32 API does not expose a
|
|
// portable descriptor-relative equivalent of POSIX openat, so a hostile local actor can still
|
|
// replace a not-yet-opened normal component between absolute-path opens. Installation directories
|
|
// therefore need trusted local filesystem/ACL ownership on Windows.
|
|
func ReadCanonicalRegular(path string, maximum int64) ([]byte, error) {
|
|
if err := ValidateCanonicalPath(path); err != nil {
|
|
return nil, err
|
|
}
|
|
volume := filepath.VolumeName(path)
|
|
root := volume + string(filepath.Separator)
|
|
components := strings.Split(strings.TrimPrefix(path, root), string(filepath.Separator))
|
|
if volume == "" || len(components) == 0 || components[0] == "" {
|
|
return nil, ErrUnsafeFile
|
|
}
|
|
|
|
current := root
|
|
parents := make([]windows.Handle, 0, len(components)-1)
|
|
defer func() { closeWindowsHandles(parents) }()
|
|
for _, component := range components[:len(components)-1] {
|
|
current = filepath.Join(current, component)
|
|
handle, err := openWindowsComponent(current, true)
|
|
if err != nil {
|
|
return nil, ErrUnsafeFile
|
|
}
|
|
parents = append(parents, handle)
|
|
}
|
|
|
|
current = filepath.Join(current, components[len(components)-1])
|
|
handle, err := openWindowsComponent(current, false)
|
|
if err != nil {
|
|
return nil, ErrUnsafeFile
|
|
}
|
|
file := os.NewFile(uintptr(handle), "thothctl-safeio")
|
|
if file == nil {
|
|
windows.CloseHandle(handle)
|
|
return nil, ErrUnsafeFile
|
|
}
|
|
defer file.Close()
|
|
return readBoundedRegularFile(file, maximum)
|
|
}
|
|
|
|
func openWindowsComponent(path string, directory bool) (windows.Handle, error) {
|
|
flags := uint32(windows.FILE_FLAG_OPEN_REPARSE_POINT)
|
|
if directory {
|
|
flags |= windows.FILE_FLAG_BACKUP_SEMANTICS
|
|
} else {
|
|
flags |= windows.FILE_ATTRIBUTE_NORMAL
|
|
}
|
|
handle, err := windows.CreateFile(
|
|
windows.StringToUTF16Ptr(path),
|
|
windows.GENERIC_READ,
|
|
windowsRetainedHandleShareMode,
|
|
nil,
|
|
windows.OPEN_EXISTING,
|
|
flags,
|
|
0,
|
|
)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
var information windows.ByHandleFileInformation
|
|
if err := windows.GetFileInformationByHandle(handle, &information); err != nil {
|
|
windows.CloseHandle(handle)
|
|
return 0, err
|
|
}
|
|
if information.FileAttributes&windows.FILE_ATTRIBUTE_REPARSE_POINT != 0 ||
|
|
(directory && information.FileAttributes&windows.FILE_ATTRIBUTE_DIRECTORY == 0) ||
|
|
(!directory && information.FileAttributes&windows.FILE_ATTRIBUTE_DIRECTORY != 0) {
|
|
windows.CloseHandle(handle)
|
|
return 0, ErrUnsafeFile
|
|
}
|
|
return handle, nil
|
|
}
|
|
|
|
func closeWindowsHandles(handles []windows.Handle) {
|
|
for _, handle := range handles {
|
|
windows.CloseHandle(handle)
|
|
}
|
|
}
|
|
|
|
func writeCanonicalExclusive(path string, contents []byte, mode fs.FileMode) error {
|
|
if err := ValidateCanonicalPath(path); err != nil || len(contents) > 16<<20 || mode.Perm() == 0 || mode.Perm()&0o077 != 0 {
|
|
return ErrUnsafeFile
|
|
}
|
|
h, err := windows.CreateFile(windows.StringToUTF16Ptr(path), windows.GENERIC_WRITE, 0, nil, windows.CREATE_NEW, windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_OPEN_REPARSE_POINT, 0)
|
|
if err != nil {
|
|
return ErrUnsafeFile
|
|
}
|
|
f := os.NewFile(uintptr(h), "thothctl-safeio-output")
|
|
if f == nil {
|
|
windows.CloseHandle(h)
|
|
return ErrUnsafeFile
|
|
}
|
|
defer f.Close()
|
|
if _, err := f.Write(contents); err != nil {
|
|
return ErrUnsafeFile
|
|
}
|
|
return f.Sync()
|
|
}
|
|
|
|
func validateCanonicalOutputPath(path string) error {
|
|
if err := ValidateCanonicalPath(path); err != nil {
|
|
return err
|
|
}
|
|
volume := filepath.VolumeName(path)
|
|
root := volume + string(filepath.Separator)
|
|
components := strings.Split(strings.TrimPrefix(path, root), string(filepath.Separator))
|
|
if volume == "" || len(components) < 2 || components[0] == "" {
|
|
return ErrUnsafeFile
|
|
}
|
|
current := root
|
|
for _, component := range components[:len(components)-1] {
|
|
current = filepath.Join(current, component)
|
|
h, err := openWindowsComponent(current, true)
|
|
if err != nil {
|
|
return ErrUnsafeFile
|
|
}
|
|
windows.CloseHandle(h)
|
|
}
|
|
if _, err := os.Lstat(filepath.Join(current, components[len(components)-1])); err == nil || !os.IsNotExist(err) {
|
|
return ErrUnsafeFile
|
|
}
|
|
return nil
|
|
}
|