fix(auth): harden restore verification transaction
This commit is contained in:
@@ -3,57 +3,45 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/safeio"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func replaceRestoreFile(target string, contents []byte, mode os.FileMode) error {
|
||||
if safeio.ValidateCanonicalPath(target) != nil || mode&os.ModeType != 0 || mode.Perm() == 0 {
|
||||
// replaceRestoreFile deliberately supports only owner-private Windows parents. The retained
|
||||
// native directory handle pins every ancestor, rejects reparse components, creates an owner-only
|
||||
// temporary file, and publishes it by an NT RootDirectory-relative atomic rename. Installations
|
||||
// whose restore targets do not satisfy that custody contract fail closed instead of falling back
|
||||
// to a path-based replacement.
|
||||
func replaceRestoreFile(target string, contents []byte, mode os.FileMode) (resultErr error) {
|
||||
if safeio.ValidateCanonicalPath(target) != nil || mode&os.ModeType != 0 || mode.Perm() == 0 || len(contents) == 0 {
|
||||
return safeio.ErrUnsafeFile
|
||||
}
|
||||
parent := filepath.Dir(target)
|
||||
resolved, err := filepath.EvalSymlinks(parent)
|
||||
if err != nil || resolved != parent || !safeWindowsRestoreTarget(target) {
|
||||
parent, found, err := safeio.OpenPrivateDirectory(filepath.Dir(target), false)
|
||||
if err != nil || !found || parent == nil {
|
||||
return safeio.ErrUnsafeFile
|
||||
}
|
||||
temporary, err := os.CreateTemp(parent, ".tht-restore-*.tmp")
|
||||
defer func() {
|
||||
if closeErr := parent.Close(); closeErr != nil {
|
||||
resultErr = safeio.ErrUnsafeFile
|
||||
}
|
||||
}()
|
||||
safeio.NotifyPrivateDirectoryTestHookForTest("after-restore-parent-open")
|
||||
if parent.Validate() != nil {
|
||||
return safeio.ErrUnsafeFile
|
||||
}
|
||||
created, err := parent.CreateRegular(filepath.Base(target), contents)
|
||||
if err != nil {
|
||||
return safeio.ErrUnsafeFile
|
||||
}
|
||||
temporaryPath := temporary.Name()
|
||||
defer os.Remove(temporaryPath)
|
||||
if err := temporary.Chmod(mode.Perm()); err == nil {
|
||||
_, err = temporary.Write(contents)
|
||||
if !created {
|
||||
if err := parent.ReplaceRegular(filepath.Base(target), contents); err != nil {
|
||||
return safeio.ErrUnsafeFile
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
err = temporary.Sync()
|
||||
}
|
||||
closeErr := temporary.Close()
|
||||
if err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
if err != nil || !safeWindowsRestoreTarget(target) {
|
||||
return safeio.ErrUnsafeFile
|
||||
}
|
||||
from, fromErr := windows.UTF16PtrFromString(temporaryPath)
|
||||
to, toErr := windows.UTF16PtrFromString(target)
|
||||
if fromErr != nil || toErr != nil {
|
||||
return safeio.ErrUnsafeFile
|
||||
}
|
||||
if err := windows.MoveFileEx(from, to, windows.MOVEFILE_REPLACE_EXISTING|windows.MOVEFILE_WRITE_THROUGH); err != nil {
|
||||
if parent.Validate() != nil {
|
||||
return safeio.ErrUnsafeFile
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func safeWindowsRestoreTarget(target string) bool {
|
||||
info, err := os.Lstat(target)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return true
|
||||
}
|
||||
return err == nil && info.Mode().IsRegular() && info.Mode()&os.ModeSymlink == 0
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user