mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-25 09:01:29 +02:00
127 lines
4.9 KiB
Go
127 lines
4.9 KiB
Go
package elevate
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
// A file the test user created under their own profile, which is what a per-user
|
|
// install looks like. The whole chain up to the volume root is walked, so this is
|
|
// also what says the walk does not refuse an ordinary Windows installation: the
|
|
// root of every volume grants BUILTIN\Users rights that are not ours to worry
|
|
// about.
|
|
func TestCheckOnlyOwnerWritableAcceptsOwnFile(t *testing.T) {
|
|
err := checkOnlyOwnerWritable(writeExecutable(t))
|
|
assert.NoError(t, err, "a file the test user owns, under directories only administrators can write")
|
|
}
|
|
|
|
// Write access held by an account that cannot answer the UAC prompt means that
|
|
// account decides what runs behind it, whoever the ACE names. The trustees that
|
|
// must not have it cannot be listed, so the check names the ones that may.
|
|
func TestCheckOnlyOwnerWritableRejectsUntrustedWriters(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
wellKnown windows.WELL_KNOWN_SID_TYPE
|
|
}{
|
|
{name: "everyone", wellKnown: windows.WinWorldSid},
|
|
{name: "authenticated users", wellKnown: windows.WinAuthenticatedUserSid},
|
|
{name: "builtin users", wellKnown: windows.WinBuiltinUsersSid},
|
|
// A service account, which no denylist of the obvious groups would name
|
|
// and which cannot elevate any more than Everyone can.
|
|
{name: "local service", wellKnown: windows.WinLocalServiceSid},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
path := writeExecutable(t)
|
|
grantWrite(t, path, tt.wellKnown)
|
|
|
|
assert.Error(t, checkOnlyOwnerWritable(path),
|
|
"write access for %s must be refused", tt.name)
|
|
})
|
|
}
|
|
}
|
|
|
|
// The masks are the policy: on a file any write reaches its contents, while on a
|
|
// directory only deleting or taking over an entry reaches something already
|
|
// there. Adding an entry does not, which is why the walk survives a volume root.
|
|
func TestWriteAccessMasks(t *testing.T) {
|
|
assert.NotZero(t, fileWriteAccess&windows.FILE_WRITE_DATA, "writing a file's data reaches its contents")
|
|
assert.NotZero(t, fileWriteAccess&windows.FILE_APPEND_DATA, "appending to a file reaches its contents")
|
|
|
|
assert.Zero(t, dirWriteAccess&windows.FILE_WRITE_DATA, "adding a file to a directory replaces nothing")
|
|
assert.Zero(t, dirWriteAccess&windows.FILE_APPEND_DATA, "adding a subdirectory replaces nothing")
|
|
assert.NotZero(t, dirWriteAccess&fileDeleteChild, "deleting an entry replaces it")
|
|
assert.NotZero(t, dirWriteAccess&windows.DELETE, "deleting the directory takes its entries with it")
|
|
}
|
|
|
|
func TestIsAllowACE(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
aceType uint8
|
|
want bool
|
|
}{
|
|
{name: "allowed", aceType: windows.ACCESS_ALLOWED_ACE_TYPE, want: true},
|
|
{name: "allowed callback", aceType: accessAllowedCallbackACEType, want: true},
|
|
{name: "allowed object", aceType: accessAllowedObjectACEType, want: true},
|
|
{name: "allowed callback object", aceType: accessAllowedCallbackObjectACEType, want: true},
|
|
{name: "denied", aceType: windows.ACCESS_DENIED_ACE_TYPE},
|
|
// SYSTEM_AUDIT_ACE_TYPE, which x/sys does not define: an ACE that records
|
|
// access rather than granting it.
|
|
{name: "audit", aceType: 0x2},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert.Equal(t, tt.want, isAllowACE(tt.aceType), "ACE type %#x", tt.aceType)
|
|
})
|
|
}
|
|
}
|
|
|
|
// writeExecutable creates a plain file under the test's own directory, the shape
|
|
// trustedSelf checks.
|
|
func writeExecutable(t *testing.T) string {
|
|
t.Helper()
|
|
path := filepath.Join(t.TempDir(), "netbird-ui.exe")
|
|
require.NoError(t, os.WriteFile(path, []byte("MZ"), 0o755), "write the executable")
|
|
return path
|
|
}
|
|
|
|
// grantWrite replaces the file's DACL with one that grants a well-known trustee
|
|
// everything, keeping the test user's own access so the file stays deletable.
|
|
func grantWrite(t *testing.T, path string, wellKnown windows.WELL_KNOWN_SID_TYPE) {
|
|
t.Helper()
|
|
|
|
trustee, err := windows.CreateWellKnownSid(wellKnown)
|
|
require.NoError(t, err, "build the trustee SID")
|
|
self, err := currentUserSID()
|
|
require.NoError(t, err, "read the test user's SID")
|
|
|
|
acl, err := windows.ACLFromEntries([]windows.EXPLICIT_ACCESS{
|
|
fullControl(self, windows.TRUSTEE_IS_USER),
|
|
fullControl(trustee, windows.TRUSTEE_IS_WELL_KNOWN_GROUP),
|
|
}, nil)
|
|
require.NoError(t, err, "build the ACL")
|
|
|
|
require.NoError(t, windows.SetNamedSecurityInfo(path, windows.SE_FILE_OBJECT,
|
|
windows.DACL_SECURITY_INFORMATION|windows.PROTECTED_DACL_SECURITY_INFORMATION,
|
|
nil, nil, acl, nil), "set the DACL")
|
|
}
|
|
|
|
func fullControl(sid *windows.SID, trusteeType uint32) windows.EXPLICIT_ACCESS {
|
|
return windows.EXPLICIT_ACCESS{
|
|
AccessPermissions: windows.GENERIC_ALL,
|
|
AccessMode: windows.GRANT_ACCESS,
|
|
Trustee: windows.TRUSTEE{
|
|
TrusteeForm: windows.TRUSTEE_IS_SID,
|
|
TrusteeType: windows.TRUSTEE_TYPE(trusteeType),
|
|
TrusteeValue: windows.TrusteeValueFromSID(sid),
|
|
},
|
|
}
|
|
}
|