fix: close workspace preprocessing contract gaps
This commit is contained in:
@@ -75,3 +75,51 @@ func TestWriteCanonicalExclusiveRejectsExistingAndCreatesPrivateFile(t *testing.
|
||||
t.Fatalf("replacement = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadCanonicalRegularRejectsHardlinkAndDirectory(t *testing.T) {
|
||||
root, err := filepath.EvalSymlinks(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
original := filepath.Join(root, "original.sql")
|
||||
if err := os.WriteFile(original, []byte("select 1"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
hardlink := filepath.Join(root, "hardlink.sql")
|
||||
if err := os.Link(original, hardlink); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := ReadCanonicalRegular(hardlink, 1024); !errors.Is(err, ErrUnsafeFile) {
|
||||
t.Fatalf("hardlink error = %v, want ErrUnsafeFile", err)
|
||||
}
|
||||
directory := filepath.Join(root, "directory.sql")
|
||||
if err := os.Mkdir(directory, 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := ReadCanonicalRegular(directory, 1024); !errors.Is(err, ErrUnsafeFile) {
|
||||
t.Fatalf("directory error = %v, want ErrUnsafeFile", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateCanonicalOutputPathRejectsExistingDirectoryAndSymlink(t *testing.T) {
|
||||
root, err := filepath.EvalSymlinks(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
directory := filepath.Join(root, "existing.yaml")
|
||||
if err := os.Mkdir(directory, 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ValidateCanonicalOutputPath(directory); !errors.Is(err, ErrUnsafeFile) {
|
||||
t.Fatalf("existing directory error = %v, want ErrUnsafeFile", err)
|
||||
}
|
||||
target := filepath.Join(root, "target.yaml")
|
||||
if err := os.WriteFile(target, []byte("target"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
link := filepath.Join(root, "link.yaml")
|
||||
testsupport.SymlinkOrSkip(t, target, link)
|
||||
if err := ValidateCanonicalOutputPath(link); !errors.Is(err, ErrUnsafeFile) {
|
||||
t.Fatalf("output symlink error = %v, want ErrUnsafeFile", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ func closeUnixDescriptors(descriptors []int) {
|
||||
}
|
||||
|
||||
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 {
|
||||
if err := ValidateCanonicalPath(path); err != nil || len(contents) > 16<<20 || mode.Perm() != 0o600 {
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
components := strings.Split(strings.TrimPrefix(path, string(os.PathSeparator)), string(os.PathSeparator))
|
||||
|
||||
@@ -6,8 +6,11 @@ import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
@@ -119,7 +122,7 @@ func closeWindowsHandles(handles []windows.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 {
|
||||
if err := ValidateCanonicalPath(path); err != nil || len(contents) > 16<<20 || mode.Perm() != 0o600 {
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
parent, retainedParents, err := openWindowsParents(path)
|
||||
@@ -129,7 +132,11 @@ func writeCanonicalExclusive(path string, contents []byte, mode fs.FileMode) err
|
||||
defer closeWindowsHandles(retainedParents)
|
||||
// Parent handles stay open with delete sharing denied until publication and
|
||||
// identity recheck complete; this is the Windows equivalent of retained dirfds.
|
||||
h, err := windows.CreateFile(windows.StringToUTF16Ptr(filepath.Join(parent, filepath.Base(path))), windows.GENERIC_WRITE, 0, nil, windows.CREATE_NEW, windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_OPEN_REPARSE_POINT, 0)
|
||||
securityDescriptor, securityAttributes, err := ownerOnlySecurityAttributes()
|
||||
if err != nil {
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
h, err := windows.CreateFile(windows.StringToUTF16Ptr(filepath.Join(parent, filepath.Base(path))), windows.GENERIC_WRITE, 0, securityAttributes, windows.CREATE_NEW, windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_OPEN_REPARSE_POINT, 0)
|
||||
if err != nil {
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
@@ -139,7 +146,11 @@ func writeCanonicalExclusive(path string, contents []byte, mode fs.FileMode) err
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
defer f.Close()
|
||||
if err := f.Chmod(mode); err != nil {
|
||||
// Go's Windows Chmod only toggles the read-only attribute; it cannot enforce
|
||||
// owner-only permissions. The restrictive DACL is installed at creation time
|
||||
// through SECURITY_ATTRIBUTES above.
|
||||
_ = securityDescriptor
|
||||
if mode.Perm() != 0o600 {
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
if _, err := f.Write(contents); err != nil {
|
||||
@@ -202,3 +213,47 @@ func validateCanonicalOutputPath(path string) error {
|
||||
func sameWindowsFile(a, b windows.ByHandleFileInformation) bool {
|
||||
return a.VolumeSerialNumber == b.VolumeSerialNumber && a.FileIndexHigh == b.FileIndexHigh && a.FileIndexLow == b.FileIndexLow
|
||||
}
|
||||
|
||||
// ownerOnlySecurityAttributes builds a non-inheriting DACL granting the current
|
||||
// token's user read/write access and no access to inherited or other trustees.
|
||||
// The descriptor is retained by the returned Go value for the duration of CreateFile.
|
||||
func ownerOnlySecurityAttributes() (*windows.SECURITY_DESCRIPTOR, *windows.SecurityAttributes, error) {
|
||||
user, err := windows.GetCurrentProcessToken().GetTokenUser()
|
||||
if err != nil || user == nil || user.User.Sid == nil {
|
||||
return nil, nil, ErrUnsafeFile
|
||||
}
|
||||
var pinner runtime.Pinner
|
||||
pinner.Pin(user.User.Sid)
|
||||
defer pinner.Unpin()
|
||||
trustee := windows.TRUSTEE{
|
||||
TrusteeForm: windows.TRUSTEE_IS_SID,
|
||||
TrusteeType: windows.TRUSTEE_IS_USER,
|
||||
TrusteeValue: windows.TrusteeValueFromSID(user.User.Sid),
|
||||
}
|
||||
entries := []windows.EXPLICIT_ACCESS{{
|
||||
AccessPermissions: windows.FILE_GENERIC_READ | windows.FILE_GENERIC_WRITE,
|
||||
AccessMode: windows.SET_ACCESS,
|
||||
Inheritance: windows.NO_INHERITANCE,
|
||||
Trustee: trustee,
|
||||
}}
|
||||
descriptor, err := windows.BuildSecurityDescriptor(nil, nil, entries, nil, nil)
|
||||
if err != nil || descriptor == nil {
|
||||
return nil, nil, ErrUnsafeFile
|
||||
}
|
||||
acl, _, err := descriptor.DACL()
|
||||
if err != nil || acl == nil {
|
||||
return nil, nil, ErrUnsafeFile
|
||||
}
|
||||
if err := descriptor.SetControl(windows.SE_DACL_PROTECTED, windows.SE_DACL_PROTECTED); err != nil {
|
||||
return nil, nil, ErrUnsafeFile
|
||||
}
|
||||
// BuildSecurityDescriptor returns a self-relative descriptor. Re-using its
|
||||
// DACL as the creation descriptor is valid, and the explicit DACL has no
|
||||
// inheritable ACEs; the protected flag is applied by the kernel on creation.
|
||||
_ = acl
|
||||
attrs := &windows.SecurityAttributes{
|
||||
Length: uint32(unsafe.Sizeof(windows.SecurityAttributes{})),
|
||||
SecurityDescriptor: descriptor,
|
||||
}
|
||||
return descriptor, attrs, nil
|
||||
}
|
||||
|
||||
@@ -72,3 +72,29 @@ func TestWriteCanonicalExclusiveRequiresRestrictiveMode(t *testing.T) {
|
||||
t.Fatal("accepted non-restrictive output mode")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteCanonicalExclusiveCreatesProtectedOwnerOnlyDACL(t *testing.T) {
|
||||
root, err := filepath.EvalSymlinks(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
path := filepath.Join(root, "candidate.yaml")
|
||||
if err := writeCanonicalExclusive(path, []byte("x"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sd, err := windows.GetNamedSecurityInfo(path, windows.SE_FILE_OBJECT, windows.DACL_SECURITY_INFORMATION)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
control, _, err := sd.Control()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if control&windows.SE_DACL_PROTECTED == 0 {
|
||||
t.Fatalf("output DACL control = %#x, want protected", control)
|
||||
}
|
||||
acl, _, err := sd.DACL()
|
||||
if err != nil || acl == nil || acl.AceCount != 1 {
|
||||
t.Fatalf("output DACL = %#v, err=%v; want one owner ACE", acl, err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user