fix(auth): add Windows session storage bridge
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
//go:build !windows
|
||||
|
||||
package safeio
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func claimCanonicalPrivateRegular(source, claim string) (bool, error) {
|
||||
directory, sourceName, err := openCanonicalParentDirectory(source)
|
||||
if err != nil {
|
||||
return false, ErrUnsafeFile
|
||||
}
|
||||
defer unix.Close(directory)
|
||||
claimName := claim[len(claim)-len(claimBaseName(claim)):]
|
||||
if err := requirePrivateRegularAtLinks(directory, sourceName, 1); err != nil {
|
||||
if requireSamePrivatePairAt(directory, sourceName, claimName) == nil {
|
||||
return false, nil
|
||||
}
|
||||
if isPrivateClaimAbsentOrOrphanAt(directory, sourceName, claimName) {
|
||||
return false, nil
|
||||
}
|
||||
return false, ErrUnsafeFile
|
||||
}
|
||||
if err := unix.Linkat(directory, sourceName, directory, claimName, 0); err != nil {
|
||||
if errors.Is(err, unix.EEXIST) && privateRegularAtAllowedLinks(directory, claimName, 1, 2) == nil {
|
||||
return false, nil
|
||||
}
|
||||
return false, ErrUnsafeFile
|
||||
}
|
||||
if err := requireSamePrivatePairAt(directory, sourceName, claimName); err != nil {
|
||||
return false, ErrUnsafeFile
|
||||
}
|
||||
if err := unix.Fsync(directory); err != nil {
|
||||
return false, ErrUnsafeFile
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func readCanonicalPrivateClaim(source, claim string, maximum int64) ([]byte, bool, error) {
|
||||
directory, sourceName, err := openCanonicalParentDirectory(source)
|
||||
if err != nil {
|
||||
return nil, false, ErrUnsafeFile
|
||||
}
|
||||
defer unix.Close(directory)
|
||||
claimName := claim[len(claim)-len(claimBaseName(claim)):]
|
||||
if err := requireSamePrivatePairAt(directory, sourceName, claimName); err != nil {
|
||||
if isPrivateClaimAbsentOrOrphanAt(directory, sourceName, claimName) {
|
||||
return nil, false, nil
|
||||
}
|
||||
return nil, false, ErrUnsafeFile
|
||||
}
|
||||
descriptor, err := unix.Openat(directory, sourceName, unix.O_RDONLY|unix.O_CLOEXEC|unix.O_NOFOLLOW, 0)
|
||||
if err != nil {
|
||||
return nil, false, ErrUnsafeFile
|
||||
}
|
||||
file := os.NewFile(uintptr(descriptor), "tht-safeio-oidc-claim")
|
||||
if file == nil {
|
||||
unix.Close(descriptor)
|
||||
return nil, false, ErrUnsafeFile
|
||||
}
|
||||
defer file.Close()
|
||||
var before unix.Stat_t
|
||||
if err := unix.Fstat(descriptor, &before); err != nil || !privateRegularStatWithLinks(before, 2) {
|
||||
return nil, false, ErrUnsafeFile
|
||||
}
|
||||
contents, err := io.ReadAll(io.LimitReader(file, maximum+1))
|
||||
if err != nil || int64(len(contents)) > maximum {
|
||||
return nil, false, ErrUnsafeFile
|
||||
}
|
||||
var after unix.Stat_t
|
||||
if err := unix.Fstat(descriptor, &after); err != nil || !sameUnixPrivateFile(before, after) || !privateRegularStatWithLinks(after, 2) {
|
||||
return nil, false, ErrUnsafeFile
|
||||
}
|
||||
if err := requireSamePrivatePairAt(directory, sourceName, claimName); err != nil {
|
||||
return nil, false, ErrUnsafeFile
|
||||
}
|
||||
return contents, true, nil
|
||||
}
|
||||
|
||||
func removeCanonicalPrivateClaim(source, claim string) (bool, error) {
|
||||
directory, sourceName, err := openCanonicalParentDirectory(source)
|
||||
if err != nil {
|
||||
return false, ErrUnsafeFile
|
||||
}
|
||||
defer unix.Close(directory)
|
||||
claimName := claim[len(claim)-len(claimBaseName(claim)):]
|
||||
if err := requireSamePrivatePairAt(directory, sourceName, claimName); err != nil {
|
||||
if isPrivateClaimAbsentOrOrphanAt(directory, sourceName, claimName) {
|
||||
return false, nil
|
||||
}
|
||||
return false, ErrUnsafeFile
|
||||
}
|
||||
if err := unix.Unlinkat(directory, sourceName, 0); err != nil {
|
||||
return false, ErrUnsafeFile
|
||||
}
|
||||
if err := requirePrivateRegularAtLinks(directory, claimName, 1); err != nil {
|
||||
return false, ErrUnsafeFile
|
||||
}
|
||||
if err := unix.Unlinkat(directory, claimName, 0); err != nil {
|
||||
return false, ErrUnsafeFile
|
||||
}
|
||||
if err := unix.Fsync(directory); err != nil {
|
||||
return false, ErrUnsafeFile
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func claimBaseName(path string) string {
|
||||
for index := len(path) - 1; index >= 0; index-- {
|
||||
if path[index] == byte(os.PathSeparator) {
|
||||
return path[index+1:]
|
||||
}
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func privateRegularAtAllowedLinks(directory int, name string, allowed ...uint64) error {
|
||||
var stat unix.Stat_t
|
||||
if err := unix.Fstatat(directory, name, &stat, unix.AT_SYMLINK_NOFOLLOW); err != nil {
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
for _, links := range allowed {
|
||||
if privateRegularStatWithLinks(stat, links) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
|
||||
func requirePrivateRegularAtLinks(directory int, name string, links uint64) error {
|
||||
return privateRegularAtAllowedLinks(directory, name, links)
|
||||
}
|
||||
|
||||
func requireSamePrivatePairAt(directory int, source, claim string) error {
|
||||
var left, right unix.Stat_t
|
||||
if err := unix.Fstatat(directory, source, &left, unix.AT_SYMLINK_NOFOLLOW); err != nil || !privateRegularStatWithLinks(left, 2) {
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
if err := unix.Fstatat(directory, claim, &right, unix.AT_SYMLINK_NOFOLLOW); err != nil || !privateRegularStatWithLinks(right, 2) {
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
if !sameUnixPrivateFile(left, right) {
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isPrivateClaimAbsentOrOrphanAt(directory int, source, claim string) bool {
|
||||
var sourceStat, claimStat unix.Stat_t
|
||||
sourceErr := unix.Fstatat(directory, source, &sourceStat, unix.AT_SYMLINK_NOFOLLOW)
|
||||
claimErr := unix.Fstatat(directory, claim, &claimStat, unix.AT_SYMLINK_NOFOLLOW)
|
||||
if errors.Is(sourceErr, unix.ENOENT) && errors.Is(claimErr, unix.ENOENT) {
|
||||
return true
|
||||
}
|
||||
return errors.Is(sourceErr, unix.ENOENT) && claimErr == nil && privateRegularStatWithLinks(claimStat, 1)
|
||||
}
|
||||
|
||||
func privateRegularStatWithLinks(stat unix.Stat_t, links uint64) bool {
|
||||
return stat.Mode&unix.S_IFMT == unix.S_IFREG && uint64(stat.Nlink) == links && stat.Mode&0o7777 == 0o600
|
||||
}
|
||||
|
||||
func sameUnixPrivateFile(left, right unix.Stat_t) bool {
|
||||
return left.Dev == right.Dev && left.Ino == right.Ino && left.Size == right.Size && left.Mtim == right.Mtim
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
//go:build windows
|
||||
|
||||
package safeio
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
type windowsPrivateRegular struct {
|
||||
parents *windowsParentHandles
|
||||
handle windows.Handle
|
||||
path string
|
||||
info windows.ByHandleFileInformation
|
||||
}
|
||||
|
||||
func (value *windowsPrivateRegular) Close() {
|
||||
if value.handle != 0 {
|
||||
_ = windows.CloseHandle(value.handle)
|
||||
value.handle = 0
|
||||
}
|
||||
if value.parents != nil {
|
||||
value.parents.Close()
|
||||
value.parents = nil
|
||||
}
|
||||
}
|
||||
|
||||
func openWindowsPrivateRegular(path string, links uint32) (*windowsPrivateRegular, error) {
|
||||
parents, target, err := openCanonicalWindowsParent(path)
|
||||
if err != nil || len(parents.handles) == 0 || validateOwnerOnlyDACL(parents.handles[len(parents.handles)-1]) != nil {
|
||||
if parents != nil {
|
||||
parents.Close()
|
||||
}
|
||||
return nil, ErrUnsafeFile
|
||||
}
|
||||
fullPath := filepath.Join(parents.directory, target)
|
||||
handle, err := windows.CreateFile(
|
||||
windows.StringToUTF16Ptr(fullPath),
|
||||
windows.GENERIC_READ,
|
||||
windowsRetainedHandleShareMode,
|
||||
nil,
|
||||
windows.OPEN_EXISTING,
|
||||
windows.FILE_FLAG_OPEN_REPARSE_POINT|windows.FILE_ATTRIBUTE_NORMAL,
|
||||
0,
|
||||
)
|
||||
if err != nil {
|
||||
parents.Close()
|
||||
return nil, err
|
||||
}
|
||||
var info windows.ByHandleFileInformation
|
||||
if err := windows.GetFileInformationByHandle(handle, &info); err != nil || info.FileAttributes&windows.FILE_ATTRIBUTE_REPARSE_POINT != 0 || info.FileAttributes&windows.FILE_ATTRIBUTE_DIRECTORY != 0 || info.NumberOfLinks != links || validateOwnerOnlyDACL(handle) != nil {
|
||||
_ = windows.CloseHandle(handle)
|
||||
parents.Close()
|
||||
return nil, ErrUnsafeFile
|
||||
}
|
||||
return &windowsPrivateRegular{parents: parents, handle: handle, path: fullPath, info: info}, nil
|
||||
}
|
||||
|
||||
func claimCanonicalPrivateRegular(source, claim string) (bool, error) {
|
||||
sourceFile, err := openWindowsPrivateRegular(source, 1)
|
||||
if err != nil {
|
||||
if windowsPrivateClaimPairExists(source, claim) {
|
||||
return false, nil
|
||||
}
|
||||
if isWindowsNotFound(err) && windowsPrivateClaimAbsentOrOrphan(claim) {
|
||||
return false, nil
|
||||
}
|
||||
return false, ErrUnsafeFile
|
||||
}
|
||||
defer sourceFile.Close()
|
||||
if err := windows.CreateHardLink(windows.StringToUTF16Ptr(claim), windows.StringToUTF16Ptr(sourceFile.path), 0); err != nil {
|
||||
if errors.Is(err, windows.ERROR_FILE_EXISTS) || errors.Is(err, windows.ERROR_ALREADY_EXISTS) {
|
||||
if existing, existingErr := openWindowsPrivateRegularWithAllowedLinks(claim, 1, 2); existingErr == nil {
|
||||
existing.Close()
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
return false, ErrUnsafeFile
|
||||
}
|
||||
if err := windows.GetFileInformationByHandle(sourceFile.handle, &sourceFile.info); err != nil || sourceFile.info.NumberOfLinks != 2 {
|
||||
return false, ErrUnsafeFile
|
||||
}
|
||||
claimFile, err := openWindowsPrivateRegular(claim, 2)
|
||||
if err != nil {
|
||||
return false, ErrUnsafeFile
|
||||
}
|
||||
defer claimFile.Close()
|
||||
if !sameWindowsPrivateFile(sourceFile.info, claimFile.info) {
|
||||
return false, ErrUnsafeFile
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func readCanonicalPrivateClaim(source, claim string, maximum int64) ([]byte, bool, error) {
|
||||
sourceFile, err := openWindowsPrivateRegular(source, 2)
|
||||
if err != nil {
|
||||
if isWindowsNotFound(err) && windowsPrivateClaimAbsentOrOrphan(claim) {
|
||||
return nil, false, nil
|
||||
}
|
||||
return nil, false, ErrUnsafeFile
|
||||
}
|
||||
defer sourceFile.Close()
|
||||
claimFile, err := openWindowsPrivateRegular(claim, 2)
|
||||
if err != nil {
|
||||
if isWindowsNotFound(err) {
|
||||
return nil, false, nil
|
||||
}
|
||||
return nil, false, ErrUnsafeFile
|
||||
}
|
||||
defer claimFile.Close()
|
||||
if !sameWindowsPrivateFile(sourceFile.info, claimFile.info) {
|
||||
return nil, false, ErrUnsafeFile
|
||||
}
|
||||
file := os.NewFile(uintptr(sourceFile.handle), "tht-safeio-oidc-claim")
|
||||
if file == nil {
|
||||
return nil, false, ErrUnsafeFile
|
||||
}
|
||||
contents, readErr := io.ReadAll(io.LimitReader(file, maximum+1))
|
||||
file.Close()
|
||||
sourceFile.handle = 0
|
||||
if readErr != nil || int64(len(contents)) > maximum {
|
||||
return nil, false, ErrUnsafeFile
|
||||
}
|
||||
if err := windows.GetFileInformationByHandle(claimFile.handle, &claimFile.info); err != nil || claimFile.info.NumberOfLinks != 2 || !sameWindowsPrivateFile(sourceFile.info, claimFile.info) {
|
||||
return nil, false, ErrUnsafeFile
|
||||
}
|
||||
return contents, true, nil
|
||||
}
|
||||
|
||||
func removeCanonicalPrivateClaim(source, claim string) (bool, error) {
|
||||
sourceFile, err := openWindowsPrivateRegular(source, 2)
|
||||
if err != nil {
|
||||
if isWindowsNotFound(err) && windowsPrivateClaimAbsentOrOrphan(claim) {
|
||||
return false, nil
|
||||
}
|
||||
return false, ErrUnsafeFile
|
||||
}
|
||||
claimFile, err := openWindowsPrivateRegular(claim, 2)
|
||||
if err != nil {
|
||||
sourceFile.Close()
|
||||
if isWindowsNotFound(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, ErrUnsafeFile
|
||||
}
|
||||
if !sameWindowsPrivateFile(sourceFile.info, claimFile.info) {
|
||||
sourceFile.Close()
|
||||
claimFile.Close()
|
||||
return false, ErrUnsafeFile
|
||||
}
|
||||
sourceFile.Close()
|
||||
claimFile.Close()
|
||||
if err := windows.DeleteFile(windows.StringToUTF16Ptr(source)); err != nil {
|
||||
return false, ErrUnsafeFile
|
||||
}
|
||||
remaining, err := openWindowsPrivateRegular(claim, 1)
|
||||
if err != nil {
|
||||
return false, ErrUnsafeFile
|
||||
}
|
||||
remaining.Close()
|
||||
if err := windows.DeleteFile(windows.StringToUTF16Ptr(claim)); err != nil {
|
||||
return false, ErrUnsafeFile
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func openWindowsPrivateRegularWithAllowedLinks(path string, allowed ...uint32) (*windowsPrivateRegular, error) {
|
||||
for _, links := range allowed {
|
||||
value, err := openWindowsPrivateRegular(path, links)
|
||||
if err == nil {
|
||||
return value, nil
|
||||
}
|
||||
}
|
||||
return nil, ErrUnsafeFile
|
||||
}
|
||||
|
||||
func windowsPrivateClaimAbsentOrOrphan(claim string) bool {
|
||||
claimFile, err := openWindowsPrivateRegular(claim, 1)
|
||||
if err != nil {
|
||||
return isWindowsNotFound(err)
|
||||
}
|
||||
claimFile.Close()
|
||||
return true
|
||||
}
|
||||
|
||||
func windowsPrivateClaimPairExists(source, claim string) bool {
|
||||
sourceFile, sourceErr := openWindowsPrivateRegular(source, 2)
|
||||
if sourceErr != nil {
|
||||
return false
|
||||
}
|
||||
defer sourceFile.Close()
|
||||
claimFile, claimErr := openWindowsPrivateRegular(claim, 2)
|
||||
if claimErr != nil {
|
||||
return false
|
||||
}
|
||||
defer claimFile.Close()
|
||||
return sameWindowsPrivateFile(sourceFile.info, claimFile.info)
|
||||
}
|
||||
|
||||
func isWindowsNotFound(err error) bool {
|
||||
return errors.Is(err, windows.ERROR_FILE_NOT_FOUND) || errors.Is(err, windows.ERROR_PATH_NOT_FOUND)
|
||||
}
|
||||
|
||||
func sameWindowsPrivateFile(left, right windows.ByHandleFileInformation) bool {
|
||||
return left.VolumeSerialNumber == right.VolumeSerialNumber && left.FileIndexHigh == right.FileIndexHigh && left.FileIndexLow == right.FileIndexLow && left.FileSizeHigh == right.FileSizeHigh && left.FileSizeLow == right.FileSizeLow && left.LastWriteTime == right.LastWriteTime
|
||||
}
|
||||
@@ -71,6 +71,50 @@ func ReadCanonicalUTF8(path string, maximum int64) (string, error) {
|
||||
return string(contents), nil
|
||||
}
|
||||
|
||||
// ReadCanonicalPrivateRegular reads one owner-only private record and revalidates its metadata
|
||||
// after the bounded read. It intentionally rejects ordinary hard links; OIDC's explicitly named
|
||||
// atomic claim pair uses ReadCanonicalPrivateClaim instead.
|
||||
func ReadCanonicalPrivateRegular(path string, maximum int64) ([]byte, error) {
|
||||
return readCanonicalPrivateRegular(path, maximum)
|
||||
}
|
||||
|
||||
// PrivateDirectoryEntry is a bounded, untrusted directory listing item. Callers must still
|
||||
// validate each filename and record before using it.
|
||||
type PrivateDirectoryEntry struct {
|
||||
Name string
|
||||
ModifiedUnixMs int64
|
||||
}
|
||||
|
||||
// ListCanonicalPrivateDirectory lists regular, non-symlinked direct children from an owner-only
|
||||
// directory. It returns no content and bounds the number of entries before allocating output.
|
||||
func ListCanonicalPrivateDirectory(path string, maximumEntries int) ([]PrivateDirectoryEntry, error) {
|
||||
if maximumEntries < 1 || maximumEntries > 4096 {
|
||||
return nil, ErrUnsafeFile
|
||||
}
|
||||
if err := ValidatePrivateDirectory(path); err != nil {
|
||||
return nil, ErrUnsafeFile
|
||||
}
|
||||
entries, err := os.ReadDir(path)
|
||||
if err != nil || len(entries) > maximumEntries {
|
||||
return nil, ErrUnsafeFile
|
||||
}
|
||||
result := make([]PrivateDirectoryEntry, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
if entry.Name() == "" || strings.Contains(entry.Name(), string(filepath.Separator)) {
|
||||
return nil, ErrUnsafeFile
|
||||
}
|
||||
info, err := os.Lstat(filepath.Join(path, entry.Name()))
|
||||
if err != nil || !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 {
|
||||
return nil, ErrUnsafeFile
|
||||
}
|
||||
result = append(result, PrivateDirectoryEntry{Name: entry.Name(), ModifiedUnixMs: info.ModTime().UnixMilli()})
|
||||
}
|
||||
if err := ValidatePrivateDirectory(path); err != nil {
|
||||
return nil, ErrUnsafeFile
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func WriteCanonicalNewFile(path string, contents []byte, mode os.FileMode) error {
|
||||
if err := ValidateCanonicalPath(path); err != nil {
|
||||
return err
|
||||
@@ -127,6 +171,42 @@ func RemoveCanonicalPrivateRegular(path string) error {
|
||||
return removeCanonicalPrivateRegular(path)
|
||||
}
|
||||
|
||||
// ClaimCanonicalPrivateRegular atomically creates a second, explicit private hard link to one
|
||||
// existing record. It is used only for digest-named, single-use OIDC state claims.
|
||||
func ClaimCanonicalPrivateRegular(source, claim string) (bool, error) {
|
||||
if err := validateClaimPaths(source, claim); err != nil {
|
||||
return false, ErrUnsafeFile
|
||||
}
|
||||
return claimCanonicalPrivateRegular(source, claim)
|
||||
}
|
||||
|
||||
// ReadCanonicalPrivateClaim reads a verified two-link source/claim pair. found=false means the
|
||||
// state has already been consumed or a winning process is between its two removal steps.
|
||||
func ReadCanonicalPrivateClaim(source, claim string, maximum int64) ([]byte, bool, error) {
|
||||
if maximum < 0 || maximum == int64(^uint64(0)>>1) || validateClaimPaths(source, claim) != nil {
|
||||
return nil, false, ErrUnsafeFile
|
||||
}
|
||||
return readCanonicalPrivateClaim(source, claim, maximum)
|
||||
}
|
||||
|
||||
// RemoveCanonicalPrivateClaim removes exactly a verified two-link source/claim pair.
|
||||
func RemoveCanonicalPrivateClaim(source, claim string) (bool, error) {
|
||||
if err := validateClaimPaths(source, claim); err != nil {
|
||||
return false, ErrUnsafeFile
|
||||
}
|
||||
return removeCanonicalPrivateClaim(source, claim)
|
||||
}
|
||||
|
||||
func validateClaimPaths(source, claim string) error {
|
||||
if ValidateCanonicalPath(source) != nil || ValidateCanonicalPath(claim) != nil || filepath.Dir(source) != filepath.Dir(claim) {
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
if err := ValidatePrivateDirectory(filepath.Dir(source)); err != nil {
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func randomTemporaryName() (string, error) {
|
||||
bytes := make([]byte, 16)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
//go:build !windows
|
||||
|
||||
package safeio
|
||||
|
||||
func readCanonicalPrivateRegular(path string, maximum int64) ([]byte, error) {
|
||||
if err := ValidatePrivateRegular(path); err != nil {
|
||||
return nil, ErrUnsafeFile
|
||||
}
|
||||
contents, err := ReadCanonicalRegular(path, maximum)
|
||||
if err != nil {
|
||||
return nil, ErrUnsafeFile
|
||||
}
|
||||
if err := ValidatePrivateRegular(path); err != nil {
|
||||
return nil, ErrUnsafeFile
|
||||
}
|
||||
return contents, nil
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
//go:build windows
|
||||
|
||||
package safeio
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func readCanonicalPrivateRegular(path string, maximum int64) ([]byte, error) {
|
||||
if maximum < 0 || maximum == int64(^uint64(0)>>1) {
|
||||
return nil, ErrUnsafeFile
|
||||
}
|
||||
value, err := openWindowsPrivateRegular(path, 1)
|
||||
if err != nil {
|
||||
return nil, ErrUnsafeFile
|
||||
}
|
||||
defer value.Close()
|
||||
file := os.NewFile(uintptr(value.handle), "tht-safeio-private-read")
|
||||
if file == nil {
|
||||
return nil, ErrUnsafeFile
|
||||
}
|
||||
before := value.info
|
||||
contents, readErr := io.ReadAll(io.LimitReader(file, maximum+1))
|
||||
var after = value.info
|
||||
identityErr := windows.GetFileInformationByHandle(value.handle, &after)
|
||||
daclErr := validateOwnerOnlyDACL(value.handle)
|
||||
closeErr := file.Close()
|
||||
value.handle = 0
|
||||
if readErr != nil || identityErr != nil || daclErr != nil || !sameWindowsPrivateFile(before, after) || after.NumberOfLinks != 1 || closeErr != nil || int64(len(contents)) > maximum {
|
||||
return nil, ErrUnsafeFile
|
||||
}
|
||||
current, err := openWindowsPrivateRegular(path, 1)
|
||||
if err != nil {
|
||||
return nil, ErrUnsafeFile
|
||||
}
|
||||
current.Close()
|
||||
if !sameWindowsPrivateFile(before, current.info) {
|
||||
return nil, ErrUnsafeFile
|
||||
}
|
||||
return contents, nil
|
||||
}
|
||||
@@ -96,6 +96,14 @@ func ProtectPrivateRegular(path string) error {
|
||||
// createCanonicalNewPrivateFile installs the owner-only protected DACL in the CreateFile call, so
|
||||
// another mutation can never observe a newly-created lock with an inherited/default DACL.
|
||||
func createCanonicalNewPrivateFile(path string, mode os.FileMode) (*os.File, error) {
|
||||
parents, target, err := openCanonicalWindowsParent(path)
|
||||
if err != nil || len(parents.handles) == 0 || validateOwnerOnlyDACL(parents.handles[len(parents.handles)-1]) != nil {
|
||||
if parents != nil {
|
||||
parents.Close()
|
||||
}
|
||||
return nil, ErrUnsafeFile
|
||||
}
|
||||
defer parents.Close()
|
||||
security, err := newOwnerOnlySecurityDescriptor()
|
||||
if err != nil {
|
||||
return nil, ErrUnsafeFile
|
||||
@@ -106,7 +114,7 @@ func createCanonicalNewPrivateFile(path string, mode os.FileMode) (*os.File, err
|
||||
SecurityDescriptor: security.descriptor,
|
||||
}
|
||||
handle, err := windows.CreateFile(
|
||||
windows.StringToUTF16Ptr(path),
|
||||
windows.StringToUTF16Ptr(filepath.Join(parents.directory, target)),
|
||||
windows.GENERIC_WRITE,
|
||||
windowsRetainedHandleShareMode,
|
||||
attributes,
|
||||
|
||||
Reference in New Issue
Block a user