all: sync with master
This commit is contained in:
43
internal/permcheck/check_unix.go
Normal file
43
internal/permcheck/check_unix.go
Normal file
@@ -0,0 +1,43 @@
|
||||
//go:build unix
|
||||
|
||||
package permcheck
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||
)
|
||||
|
||||
// check is the Unix-specific implementation of [Check].
|
||||
func check(
|
||||
ctx context.Context,
|
||||
l *slog.Logger,
|
||||
workDir string,
|
||||
dataDir string,
|
||||
statsDir string,
|
||||
querylogDir string,
|
||||
confFilePath string,
|
||||
) {
|
||||
dirLoggger, fileLogger := l.With("type", typeDir), l.With("type", typeFile)
|
||||
|
||||
for _, ent := range entities(workDir, dataDir, statsDir, querylogDir, confFilePath) {
|
||||
if ent.Value {
|
||||
checkDir(ctx, dirLoggger, ent.Key)
|
||||
} else {
|
||||
checkFile(ctx, fileLogger, ent.Key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkDir checks the permissions of a single directory. The results are
|
||||
// logged at the appropriate level.
|
||||
func checkDir(ctx context.Context, l *slog.Logger, dirPath string) {
|
||||
checkPath(ctx, l, dirPath, aghos.DefaultPermDir)
|
||||
}
|
||||
|
||||
// checkFile checks the permissions of a single file. The results are logged at
|
||||
// the appropriate level.
|
||||
func checkFile(ctx context.Context, l *slog.Logger, filePath string) {
|
||||
checkPath(ctx, l, filePath, aghos.DefaultPermFile)
|
||||
}
|
||||
60
internal/permcheck/check_windows.go
Normal file
60
internal/permcheck/check_windows.go
Normal file
@@ -0,0 +1,60 @@
|
||||
//go:build windows
|
||||
|
||||
package permcheck
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// check is the Windows-specific implementation of [Check].
|
||||
//
|
||||
// Note, that it only checks the owner and the ACEs of the working directory.
|
||||
// This is due to the assumption that the working directory ACEs are inherited
|
||||
// by the underlying files and directories, since at least [migrate] sets this
|
||||
// inheritance mode.
|
||||
func check(ctx context.Context, l *slog.Logger, workDir, _, _, _, _ string) {
|
||||
l = l.With("type", typeDir, "path", workDir)
|
||||
|
||||
dacl, owner, err := getSecurityInfo(workDir)
|
||||
if err != nil {
|
||||
l.ErrorContext(ctx, "getting security info", slogutil.KeyError, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if !owner.IsWellKnown(windows.WinBuiltinAdministratorsSid) {
|
||||
l.WarnContext(ctx, "owner is not in administrators group")
|
||||
}
|
||||
|
||||
err = rangeACEs(dacl, func(
|
||||
hdr windows.ACE_HEADER,
|
||||
mask windows.ACCESS_MASK,
|
||||
sid *windows.SID,
|
||||
) (cont bool) {
|
||||
l.DebugContext(ctx, "checking access control entry", "mask", mask, "sid", sid)
|
||||
|
||||
warn := false
|
||||
switch {
|
||||
case hdr.AceType != windows.ACCESS_ALLOWED_ACE_TYPE:
|
||||
// Skip non-allowed ACEs.
|
||||
case !sid.IsWellKnown(windows.WinBuiltinAdministratorsSid):
|
||||
// Non-administrator ACEs should not have any access rights.
|
||||
warn = mask > 0
|
||||
default:
|
||||
// Administrators should full control access rights.
|
||||
warn = mask&fullControlMask != fullControlMask
|
||||
}
|
||||
if warn {
|
||||
l.WarnContext(ctx, "unexpected access control entry", "mask", mask, "sid", sid)
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
if err != nil {
|
||||
l.ErrorContext(ctx, "checking access control entries", slogutil.KeyError, err)
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package permcheck
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// NeedsMigration returns true if AdGuard Home files need permission migration.
|
||||
//
|
||||
// TODO(a.garipov): Consider ways to detect this better.
|
||||
func NeedsMigration(confFilePath string) (ok bool) {
|
||||
s, err := aghos.Stat(confFilePath)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
// Likely a first run. Don't check.
|
||||
return false
|
||||
}
|
||||
|
||||
log.Error("permcheck: checking if files need migration: %s", err)
|
||||
|
||||
// Unexpected error. Try to migrate just in case.
|
||||
return true
|
||||
}
|
||||
|
||||
return s.Mode().Perm() != aghos.DefaultPermFile
|
||||
}
|
||||
|
||||
// Migrate attempts to change the permissions of AdGuard Home's files. It logs
|
||||
// the results at an appropriate level.
|
||||
func Migrate(workDir, dataDir, statsDir, querylogDir, confFilePath string) {
|
||||
chmodDir(workDir)
|
||||
|
||||
chmodFile(confFilePath)
|
||||
|
||||
// TODO(a.garipov): Put all paths in one place and remove this duplication.
|
||||
chmodDir(dataDir)
|
||||
chmodDir(filepath.Join(dataDir, "filters"))
|
||||
chmodFile(filepath.Join(dataDir, "sessions.db"))
|
||||
chmodFile(filepath.Join(dataDir, "leases.json"))
|
||||
|
||||
if dataDir != querylogDir {
|
||||
chmodDir(querylogDir)
|
||||
}
|
||||
chmodFile(filepath.Join(querylogDir, "querylog.json"))
|
||||
chmodFile(filepath.Join(querylogDir, "querylog.json.1"))
|
||||
|
||||
if dataDir != statsDir {
|
||||
chmodDir(statsDir)
|
||||
}
|
||||
chmodFile(filepath.Join(statsDir, "stats.db"))
|
||||
}
|
||||
|
||||
// chmodDir changes the permissions of a single directory. The results are
|
||||
// logged at the appropriate level.
|
||||
func chmodDir(dirPath string) {
|
||||
chmodPath(dirPath, typeDir, aghos.DefaultPermDir)
|
||||
}
|
||||
|
||||
// chmodFile changes the permissions of a single file. The results are logged
|
||||
// at the appropriate level.
|
||||
func chmodFile(filePath string) {
|
||||
chmodPath(filePath, typeFile, aghos.DefaultPermFile)
|
||||
}
|
||||
|
||||
// chmodPath changes the permissions of a single filesystem entity. The results
|
||||
// are logged at the appropriate level.
|
||||
func chmodPath(entPath, fileType string, fm fs.FileMode) {
|
||||
err := aghos.Chmod(entPath, fm)
|
||||
if err == nil {
|
||||
log.Info("permcheck: changed permissions for %s %q", fileType, entPath)
|
||||
|
||||
return
|
||||
} else if errors.Is(err, os.ErrNotExist) {
|
||||
log.Debug("permcheck: changing permissions for %s %q: %s", fileType, entPath, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
log.Error(
|
||||
"permcheck: SECURITY WARNING: cannot change permissions for %s %q to %#o: %s; "+
|
||||
"this can leave your system vulnerable, see "+
|
||||
"https://adguard-dns.io/kb/adguard-home/running-securely/#os-service-concerns",
|
||||
fileType,
|
||||
entPath,
|
||||
fm,
|
||||
err,
|
||||
)
|
||||
}
|
||||
66
internal/permcheck/migrate_unix.go
Normal file
66
internal/permcheck/migrate_unix.go
Normal file
@@ -0,0 +1,66 @@
|
||||
//go:build unix
|
||||
|
||||
package permcheck
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||
)
|
||||
|
||||
// needsMigration is a Unix-specific implementation of [NeedsMigration].
|
||||
//
|
||||
// TODO(a.garipov): Consider ways to detect this better.
|
||||
func needsMigration(ctx context.Context, l *slog.Logger, _, confFilePath string) (ok bool) {
|
||||
s, err := os.Stat(confFilePath)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
// Likely a first run. Don't check.
|
||||
return false
|
||||
}
|
||||
|
||||
l.ErrorContext(ctx, "checking a need for permission migration", slogutil.KeyError, err)
|
||||
|
||||
// Unexpected error. Try to migrate just in case.
|
||||
return true
|
||||
}
|
||||
|
||||
return s.Mode().Perm() != aghos.DefaultPermFile
|
||||
}
|
||||
|
||||
// migrate is a Unix-specific implementation of [Migrate].
|
||||
func migrate(
|
||||
ctx context.Context,
|
||||
l *slog.Logger,
|
||||
workDir string,
|
||||
dataDir string,
|
||||
statsDir string,
|
||||
querylogDir string,
|
||||
confFilePath string,
|
||||
) {
|
||||
dirLoggger, fileLogger := l.With("type", typeDir), l.With("type", typeFile)
|
||||
|
||||
for _, ent := range entities(workDir, dataDir, statsDir, querylogDir, confFilePath) {
|
||||
if ent.Value {
|
||||
chmodDir(ctx, dirLoggger, ent.Key)
|
||||
} else {
|
||||
chmodFile(ctx, fileLogger, ent.Key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// chmodDir changes the permissions of a single directory. The results are
|
||||
// logged at the appropriate level.
|
||||
func chmodDir(ctx context.Context, l *slog.Logger, dirPath string) {
|
||||
chmodPath(ctx, l, dirPath, aghos.DefaultPermDir)
|
||||
}
|
||||
|
||||
// chmodFile changes the permissions of a single file. The results are logged
|
||||
// at the appropriate level.
|
||||
func chmodFile(ctx context.Context, l *slog.Logger, filePath string) {
|
||||
chmodPath(ctx, l, filePath, aghos.DefaultPermFile)
|
||||
}
|
||||
135
internal/permcheck/migrate_windows.go
Normal file
135
internal/permcheck/migrate_windows.go
Normal file
@@ -0,0 +1,135 @@
|
||||
//go:build windows
|
||||
|
||||
package permcheck
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// needsMigration is the Windows-specific implementation of [NeedsMigration].
|
||||
func needsMigration(ctx context.Context, l *slog.Logger, workDir, _ string) (ok bool) {
|
||||
l = l.With("type", typeDir, "path", workDir)
|
||||
|
||||
dacl, owner, err := getSecurityInfo(workDir)
|
||||
if err != nil {
|
||||
l.ErrorContext(ctx, "getting security info", slogutil.KeyError, err)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
if !owner.IsWellKnown(windows.WinBuiltinAdministratorsSid) {
|
||||
return true
|
||||
}
|
||||
|
||||
err = rangeACEs(dacl, func(
|
||||
hdr windows.ACE_HEADER,
|
||||
mask windows.ACCESS_MASK,
|
||||
sid *windows.SID,
|
||||
) (cont bool) {
|
||||
switch {
|
||||
case hdr.AceType != windows.ACCESS_ALLOWED_ACE_TYPE:
|
||||
// Skip non-allowed access control entries.
|
||||
l.DebugContext(ctx, "skipping deny access control entry", "sid", sid)
|
||||
case !sid.IsWellKnown(windows.WinBuiltinAdministratorsSid):
|
||||
// Non-administrator access control entries should not have any
|
||||
// access rights.
|
||||
ok = mask > 0
|
||||
default:
|
||||
// Administrators should have full control.
|
||||
ok = mask&fullControlMask != fullControlMask
|
||||
}
|
||||
|
||||
// Stop ranging if the access control entry is unexpected.
|
||||
return !ok
|
||||
})
|
||||
if err != nil {
|
||||
l.ErrorContext(ctx, "checking access control entries", slogutil.KeyError, err)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return ok
|
||||
}
|
||||
|
||||
// migrate is the Windows-specific implementation of [Migrate].
|
||||
//
|
||||
// It sets the owner to administrators and adds a full control access control
|
||||
// entry for the account. It also removes all non-administrator access control
|
||||
// entries, and keeps deny access control entries. For any created or modified
|
||||
// entry it sets the propagation flags to be inherited by child objects.
|
||||
func migrate(ctx context.Context, logger *slog.Logger, workDir, _, _, _, _ string) {
|
||||
l := logger.With("type", typeDir, "path", workDir)
|
||||
|
||||
dacl, owner, err := getSecurityInfo(workDir)
|
||||
if err != nil {
|
||||
l.ErrorContext(ctx, "getting security info", slogutil.KeyError, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
admins, err := windows.CreateWellKnownSid(windows.WinBuiltinAdministratorsSid)
|
||||
if err != nil {
|
||||
l.ErrorContext(ctx, "creating administrators sid", slogutil.KeyError, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(e.burkov): Check for duplicates?
|
||||
var accessEntries []windows.EXPLICIT_ACCESS
|
||||
var setACL bool
|
||||
// Iterate over the access control entries in DACL to determine if its
|
||||
// migration is needed.
|
||||
err = rangeACEs(dacl, func(
|
||||
hdr windows.ACE_HEADER,
|
||||
mask windows.ACCESS_MASK,
|
||||
sid *windows.SID,
|
||||
) (cont bool) {
|
||||
switch {
|
||||
case hdr.AceType != windows.ACCESS_ALLOWED_ACE_TYPE:
|
||||
// Add non-allowed access control entries as is, since they specify
|
||||
// the access restrictions, which shouldn't be lost.
|
||||
l.InfoContext(ctx, "migrating deny access control entry", "sid", sid)
|
||||
accessEntries = append(accessEntries, newDenyExplicitAccess(sid, mask))
|
||||
setACL = true
|
||||
case !sid.IsWellKnown(windows.WinBuiltinAdministratorsSid):
|
||||
// Remove non-administrator ACEs, since such accounts should not
|
||||
// have any access rights.
|
||||
l.InfoContext(ctx, "removing access control entry", "sid", sid)
|
||||
setACL = true
|
||||
default:
|
||||
// Administrators should have full control. Don't add a new entry
|
||||
// here since it will be added later in case there are other
|
||||
// required entries.
|
||||
l.InfoContext(ctx, "migrating access control entry", "sid", sid, "mask", mask)
|
||||
setACL = setACL || mask&fullControlMask != fullControlMask
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
if err != nil {
|
||||
l.ErrorContext(ctx, "ranging through access control entries", slogutil.KeyError, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if setACL {
|
||||
accessEntries = append(accessEntries, newFullExplicitAccess(admins))
|
||||
}
|
||||
|
||||
if !owner.IsWellKnown(windows.WinBuiltinAdministratorsSid) {
|
||||
l.InfoContext(ctx, "migrating owner", "sid", owner)
|
||||
owner = admins
|
||||
} else {
|
||||
l.DebugContext(ctx, "owner is already an administrator")
|
||||
owner = nil
|
||||
}
|
||||
|
||||
err = setSecurityInfo(workDir, owner, accessEntries)
|
||||
if err != nil {
|
||||
l.ErrorContext(ctx, "setting security info", slogutil.KeyError, err)
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,10 @@
|
||||
// Package permcheck contains code for simplifying permissions checks on files
|
||||
// and directories.
|
||||
//
|
||||
// TODO(a.garipov): Improve the approach on Windows.
|
||||
package permcheck
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"context"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
// File type constants for logging.
|
||||
@@ -22,65 +15,33 @@ const (
|
||||
|
||||
// Check checks the permissions on important files. It logs the results at
|
||||
// appropriate levels.
|
||||
func Check(workDir, dataDir, statsDir, querylogDir, confFilePath string) {
|
||||
checkDir(workDir)
|
||||
|
||||
checkFile(confFilePath)
|
||||
|
||||
// TODO(a.garipov): Put all paths in one place and remove this duplication.
|
||||
checkDir(dataDir)
|
||||
checkDir(filepath.Join(dataDir, "filters"))
|
||||
checkFile(filepath.Join(dataDir, "sessions.db"))
|
||||
checkFile(filepath.Join(dataDir, "leases.json"))
|
||||
|
||||
if dataDir != querylogDir {
|
||||
checkDir(querylogDir)
|
||||
}
|
||||
checkFile(filepath.Join(querylogDir, "querylog.json"))
|
||||
checkFile(filepath.Join(querylogDir, "querylog.json.1"))
|
||||
|
||||
if dataDir != statsDir {
|
||||
checkDir(statsDir)
|
||||
}
|
||||
checkFile(filepath.Join(statsDir, "stats.db"))
|
||||
func Check(
|
||||
ctx context.Context,
|
||||
l *slog.Logger,
|
||||
workDir string,
|
||||
dataDir string,
|
||||
statsDir string,
|
||||
querylogDir string,
|
||||
confFilePath string,
|
||||
) {
|
||||
check(ctx, l, workDir, dataDir, statsDir, querylogDir, confFilePath)
|
||||
}
|
||||
|
||||
// checkDir checks the permissions of a single directory. The results are
|
||||
// logged at the appropriate level.
|
||||
func checkDir(dirPath string) {
|
||||
checkPath(dirPath, typeDir, aghos.DefaultPermDir)
|
||||
// NeedsMigration returns true if AdGuard Home files need permission migration.
|
||||
func NeedsMigration(ctx context.Context, l *slog.Logger, workDir, confFilePath string) (ok bool) {
|
||||
return needsMigration(ctx, l, workDir, confFilePath)
|
||||
}
|
||||
|
||||
// checkFile checks the permissions of a single file. The results are logged at
|
||||
// the appropriate level.
|
||||
func checkFile(filePath string) {
|
||||
checkPath(filePath, typeFile, aghos.DefaultPermFile)
|
||||
}
|
||||
|
||||
// checkPath checks the permissions of a single filesystem entity. The results
|
||||
// are logged at the appropriate level.
|
||||
func checkPath(entPath, fileType string, want fs.FileMode) {
|
||||
s, err := aghos.Stat(entPath)
|
||||
if err != nil {
|
||||
logFunc := log.Error
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
logFunc = log.Debug
|
||||
}
|
||||
|
||||
logFunc("permcheck: checking %s %q: %s", fileType, entPath, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(a.garipov): Add a more fine-grained check and result reporting.
|
||||
perm := s.Mode().Perm()
|
||||
if perm != want {
|
||||
log.Info(
|
||||
"permcheck: SECURITY WARNING: %s %q has unexpected permissions %#o; want %#o",
|
||||
fileType,
|
||||
entPath,
|
||||
perm,
|
||||
want,
|
||||
)
|
||||
}
|
||||
// Migrate attempts to change the permissions of AdGuard Home's files. It logs
|
||||
// the results at an appropriate level.
|
||||
func Migrate(
|
||||
ctx context.Context,
|
||||
l *slog.Logger,
|
||||
workDir string,
|
||||
dataDir string,
|
||||
statsDir string,
|
||||
querylogDir string,
|
||||
confFilePath string,
|
||||
) {
|
||||
migrate(ctx, l, workDir, dataDir, statsDir, querylogDir, confFilePath)
|
||||
}
|
||||
|
||||
123
internal/permcheck/security_unix.go
Normal file
123
internal/permcheck/security_unix.go
Normal file
@@ -0,0 +1,123 @@
|
||||
//go:build unix
|
||||
|
||||
package permcheck
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/AdguardTeam/golibs/container"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||
)
|
||||
|
||||
// entity is a filesystem entity with a path and a flag indicating whether it is
|
||||
// a directory.
|
||||
type entity = container.KeyValue[string, bool]
|
||||
|
||||
// entities returns a list of filesystem entities that need to be ranged over.
|
||||
//
|
||||
// TODO(a.garipov): Put all paths in one place and remove this duplication.
|
||||
func entities(workDir, dataDir, statsDir, querylogDir, confFilePath string) (ents []entity) {
|
||||
ents = []entity{{
|
||||
Key: workDir,
|
||||
Value: true,
|
||||
}, {
|
||||
Key: confFilePath,
|
||||
Value: false,
|
||||
}, {
|
||||
Key: dataDir,
|
||||
Value: true,
|
||||
}, {
|
||||
Key: filepath.Join(dataDir, "filters"),
|
||||
Value: true,
|
||||
}, {
|
||||
Key: filepath.Join(dataDir, "sessions.db"),
|
||||
Value: false,
|
||||
}, {
|
||||
Key: filepath.Join(dataDir, "leases.json"),
|
||||
Value: false,
|
||||
}}
|
||||
|
||||
if dataDir != querylogDir {
|
||||
ents = append(ents, entity{
|
||||
Key: querylogDir,
|
||||
Value: true,
|
||||
})
|
||||
}
|
||||
ents = append(ents, entity{
|
||||
Key: filepath.Join(querylogDir, "querylog.json"),
|
||||
Value: false,
|
||||
}, entity{
|
||||
Key: filepath.Join(querylogDir, "querylog.json.1"),
|
||||
Value: false,
|
||||
})
|
||||
|
||||
if dataDir != statsDir {
|
||||
ents = append(ents, entity{
|
||||
Key: statsDir,
|
||||
Value: true,
|
||||
})
|
||||
}
|
||||
ents = append(ents, entity{
|
||||
Key: filepath.Join(statsDir, "stats.db"),
|
||||
})
|
||||
|
||||
return ents
|
||||
}
|
||||
|
||||
// checkPath checks the permissions of a single filesystem entity. The results
|
||||
// are logged at the appropriate level.
|
||||
func checkPath(ctx context.Context, l *slog.Logger, entPath string, want fs.FileMode) {
|
||||
l = l.With("path", entPath)
|
||||
|
||||
s, err := os.Stat(entPath)
|
||||
if err != nil {
|
||||
lvl := slog.LevelError
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
lvl = slog.LevelDebug
|
||||
}
|
||||
|
||||
l.Log(ctx, lvl, "checking permissions", slogutil.KeyError, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(a.garipov): Add a more fine-grained check and result reporting.
|
||||
perm := s.Mode().Perm()
|
||||
if perm == want {
|
||||
return
|
||||
}
|
||||
|
||||
permOct, wantOct := fmt.Sprintf("%#o", perm), fmt.Sprintf("%#o", want)
|
||||
l.WarnContext(ctx, "found unexpected permissions", "perm", permOct, "want", wantOct)
|
||||
}
|
||||
|
||||
// chmodPath changes the permissions of a single filesystem entity. The results
|
||||
// are logged at the appropriate level.
|
||||
func chmodPath(ctx context.Context, l *slog.Logger, entPath string, fm fs.FileMode) {
|
||||
var lvl slog.Level
|
||||
var msg string
|
||||
args := []any{"path", entPath}
|
||||
|
||||
switch err := os.Chmod(entPath, fm); {
|
||||
case err == nil:
|
||||
lvl = slog.LevelInfo
|
||||
msg = "changed permissions"
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
lvl = slog.LevelDebug
|
||||
msg = "checking permissions"
|
||||
args = append(args, slogutil.KeyError, err)
|
||||
default:
|
||||
lvl = slog.LevelError
|
||||
msg = "cannot change permissions; this can leave your system vulnerable, see " +
|
||||
"https://adguard-dns.io/kb/adguard-home/running-securely/#os-service-concerns"
|
||||
args = append(args, "target_perm", fmt.Sprintf("%#o", fm), slogutil.KeyError, err)
|
||||
}
|
||||
|
||||
l.Log(ctx, lvl, msg, args...)
|
||||
}
|
||||
167
internal/permcheck/security_windows.go
Normal file
167
internal/permcheck/security_windows.go
Normal file
@@ -0,0 +1,167 @@
|
||||
//go:build windows
|
||||
|
||||
package permcheck
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"unsafe"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// objectType is the type of the object for directories in context of security
|
||||
// API.
|
||||
const objectType windows.SE_OBJECT_TYPE = windows.SE_FILE_OBJECT
|
||||
|
||||
// fileDeleteChildRight is the mask bit for the right to delete a child object.
|
||||
// It seems to be missing from the [windows] package.
|
||||
//
|
||||
// See https://learn.microsoft.com/en-us/windows-hardware/drivers/ifs/access-mask.
|
||||
const fileDeleteChildRight windows.ACCESS_MASK = 0b0100_0000
|
||||
|
||||
// fullControlMask is the mask for full control access rights.
|
||||
const fullControlMask windows.ACCESS_MASK = windows.FILE_LIST_DIRECTORY |
|
||||
windows.FILE_WRITE_DATA |
|
||||
windows.FILE_APPEND_DATA |
|
||||
windows.FILE_READ_EA |
|
||||
windows.FILE_WRITE_EA |
|
||||
windows.FILE_TRAVERSE |
|
||||
fileDeleteChildRight |
|
||||
windows.FILE_READ_ATTRIBUTES |
|
||||
windows.FILE_WRITE_ATTRIBUTES |
|
||||
windows.DELETE |
|
||||
windows.READ_CONTROL |
|
||||
windows.WRITE_DAC |
|
||||
windows.WRITE_OWNER |
|
||||
windows.SYNCHRONIZE
|
||||
|
||||
// aceFunc is a function that handles access control entries in the
|
||||
// discretionary access control list. It should return true to continue
|
||||
// iterating over the entries, or false to stop.
|
||||
type aceFunc = func(
|
||||
hdr windows.ACE_HEADER,
|
||||
mask windows.ACCESS_MASK,
|
||||
sid *windows.SID,
|
||||
) (cont bool)
|
||||
|
||||
// rangeACEs ranges over the access control entries in the discretionary access
|
||||
// control list of the specified security descriptor and calls f for each one.
|
||||
func rangeACEs(dacl *windows.ACL, f aceFunc) (err error) {
|
||||
var errs []error
|
||||
for i := range uint32(dacl.AceCount) {
|
||||
var ace *windows.ACCESS_ALLOWED_ACE
|
||||
err = windows.GetAce(dacl, i, &ace)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("getting entry at index %d: %w", i, err))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
sid := (*windows.SID)(unsafe.Pointer(&ace.SidStart))
|
||||
if !f(ace.Header, ace.Mask, sid) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err = errors.Join(errs...); err != nil {
|
||||
return fmt.Errorf("checking access control entries: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setSecurityInfo sets the security information on the specified file, using
|
||||
// ents to create a discretionary access control list. Either owner or ents can
|
||||
// be nil, in which case the corresponding information is not set, but at least
|
||||
// one of them should be specified.
|
||||
func setSecurityInfo(fname string, owner *windows.SID, ents []windows.EXPLICIT_ACCESS) (err error) {
|
||||
var secInfo windows.SECURITY_INFORMATION
|
||||
|
||||
var acl *windows.ACL
|
||||
if len(ents) > 0 {
|
||||
// TODO(e.burkov): Investigate if this whole set is necessary.
|
||||
secInfo |= windows.DACL_SECURITY_INFORMATION |
|
||||
windows.PROTECTED_DACL_SECURITY_INFORMATION |
|
||||
windows.UNPROTECTED_DACL_SECURITY_INFORMATION
|
||||
|
||||
acl, err = windows.ACLFromEntries(ents, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating access control list: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if owner != nil {
|
||||
secInfo |= windows.OWNER_SECURITY_INFORMATION
|
||||
}
|
||||
|
||||
if secInfo == 0 {
|
||||
return errors.Error("no security information to set")
|
||||
}
|
||||
|
||||
err = windows.SetNamedSecurityInfo(fname, objectType, secInfo, owner, nil, acl, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting security info: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getSecurityInfo retrieves the security information for the specified file.
|
||||
func getSecurityInfo(fname string) (dacl *windows.ACL, owner *windows.SID, err error) {
|
||||
// desiredSecInfo defines the parts of a security descriptor to retrieve.
|
||||
const desiredSecInfo windows.SECURITY_INFORMATION = windows.OWNER_SECURITY_INFORMATION |
|
||||
windows.DACL_SECURITY_INFORMATION |
|
||||
windows.PROTECTED_DACL_SECURITY_INFORMATION |
|
||||
windows.UNPROTECTED_DACL_SECURITY_INFORMATION
|
||||
|
||||
sd, err := windows.GetNamedSecurityInfo(fname, objectType, desiredSecInfo)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("getting security descriptor: %w", err)
|
||||
}
|
||||
|
||||
owner, _, err = sd.Owner()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("getting owner sid: %w", err)
|
||||
}
|
||||
|
||||
dacl, _, err = sd.DACL()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("getting discretionary access control list: %w", err)
|
||||
}
|
||||
|
||||
return dacl, owner, nil
|
||||
}
|
||||
|
||||
// newFullExplicitAccess creates a new explicit access entry with full control
|
||||
// permissions.
|
||||
func newFullExplicitAccess(sid *windows.SID) (accEnt windows.EXPLICIT_ACCESS) {
|
||||
return windows.EXPLICIT_ACCESS{
|
||||
AccessPermissions: fullControlMask,
|
||||
AccessMode: windows.GRANT_ACCESS,
|
||||
Inheritance: windows.SUB_CONTAINERS_AND_OBJECTS_INHERIT,
|
||||
Trustee: windows.TRUSTEE{
|
||||
TrusteeForm: windows.TRUSTEE_IS_SID,
|
||||
TrusteeType: windows.TRUSTEE_IS_UNKNOWN,
|
||||
TrusteeValue: windows.TrusteeValueFromSID(sid),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// newDenyExplicitAccess creates a new explicit access entry with specified deny
|
||||
// permissions.
|
||||
func newDenyExplicitAccess(
|
||||
sid *windows.SID,
|
||||
mask windows.ACCESS_MASK,
|
||||
) (accEnt windows.EXPLICIT_ACCESS) {
|
||||
return windows.EXPLICIT_ACCESS{
|
||||
AccessPermissions: mask,
|
||||
AccessMode: windows.DENY_ACCESS,
|
||||
Inheritance: windows.SUB_CONTAINERS_AND_OBJECTS_INHERIT,
|
||||
Trustee: windows.TRUSTEE{
|
||||
TrusteeForm: windows.TRUSTEE_IS_SID,
|
||||
TrusteeType: windows.TRUSTEE_IS_UNKNOWN,
|
||||
TrusteeValue: windows.TrusteeValueFromSID(sid),
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user