package target import ( "errors" "io/fs" "path/filepath" "strings" "golang.org/x/sys/unix" "proxyctl/internal/profile" ) // ReadOnlyWriteAuthority observes whether the current process can write the // configured locations. It never creates a probe file or changes permissions. func ReadOnlyWriteAuthority(paths ...string) Check { check := Check{Name: "write authority", OK: true} var evidence []string for _, path := range paths { ok, detail := readOnlyWritablePath(path) check.OK = check.OK && ok evidence = append(evidence, detail) } check.Evidence = profile.RedactText(strings.Join(evidence, "; ")) return check } // readOnlyWritablePath anchors traversal at an open root descriptor and refuses // symbolic links in every component. The authority observation therefore // cannot be redirected to a victim between path lookup steps, and it never // opens victim content or mutates filesystem metadata. func readOnlyWritablePath(path string) (bool, string) { absolute, err := filepath.Abs(path) if err != nil { return false, path + " cannot be inspected: " + err.Error() } absolute = filepath.Clean(absolute) components := strings.Split(strings.TrimPrefix(absolute, string(filepath.Separator)), string(filepath.Separator)) if len(components) == 1 && components[0] == "" { components = nil } current, err := unix.Open(string(filepath.Separator), unix.O_PATH|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) if err != nil { return false, absolute + " cannot be inspected: " + err.Error() } defer func() { _ = unix.Close(current) }() currentPath := string(filepath.Separator) for index, component := range components { child, openErr := unix.Openat(current, component, unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) if errors.Is(openErr, fs.ErrNotExist) { return writableDirectory(current, currentPath, absolute) } if openErr != nil { return false, filepath.Join(currentPath, component) + " cannot be inspected without following links: " + openErr.Error() } var stat unix.Stat_t statErr := unix.Fstat(child, &stat) if statErr != nil { _ = unix.Close(child) return false, filepath.Join(currentPath, component) + " cannot be inspected: " + statErr.Error() } kind := stat.Mode & unix.S_IFMT componentPath := filepath.Join(currentPath, component) if kind == unix.S_IFLNK { _ = unix.Close(child) return false, componentPath + " is a symbolic link and is unavailable for safe write observation" } if index == len(components)-1 { _ = unix.Close(child) return writableDirectory(current, currentPath, absolute) } if kind != unix.S_IFDIR { _ = unix.Close(child) return false, componentPath + " is not a directory" } _ = unix.Close(current) current = child currentPath = componentPath } return writableDirectory(current, currentPath, absolute) } func writableDirectory(descriptor int, directory, targetPath string) (bool, string) { if err := unix.Faccessat(descriptor, ".", unix.W_OK|unix.X_OK, unix.AT_EACCESS); err != nil { return false, directory + " is not writable for " + targetPath + ": " + err.Error() } return true, directory + " is writable for " + targetPath }