fix: harden Shell Target recovery and storage

This commit is contained in:
proxyctl coordinator
2026-07-17 03:11:33 +08:00
parent 12e857932b
commit 5e0de9b766
9 changed files with 527 additions and 150 deletions
+237
View File
@@ -0,0 +1,237 @@
// Package securefs provides descriptor-anchored file operations that never
// follow path components or final-file symbolic links.
package securefs
import (
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"strings"
"golang.org/x/sys/unix"
)
type Owner struct{ UID, GID int }
func OpenNoFollow(path string, flags int, mode uint32) (*os.File, error) {
fd, err := unix.Open(path, flags|unix.O_CLOEXEC|unix.O_NOFOLLOW, mode)
if err != nil {
return nil, err
}
return os.NewFile(uintptr(fd), path), nil
}
func OpenAt(directory *os.File, name string, flags int, mode uint32) (*os.File, error) {
fd, err := unix.Openat(int(directory.Fd()), name, flags|unix.O_CLOEXEC|unix.O_NOFOLLOW, mode)
if err != nil {
return nil, err
}
return os.NewFile(uintptr(fd), filepath.Join(directory.Name(), name)), nil
}
func CreateTemporaryAt(directory *os.File, prefix string) (*os.File, string, error) {
for range 100 {
bytes := make([]byte, 8)
if _, err := rand.Read(bytes); err != nil {
return nil, "", err
}
name := prefix + hex.EncodeToString(bytes)
file, err := OpenAt(directory, name, unix.O_CREAT|unix.O_EXCL|unix.O_RDWR, 0o600)
if errors.Is(err, fs.ErrExist) {
continue
}
return file, name, err
}
return nil, "", errors.New("could not allocate a unique temporary file")
}
type Store struct {
TrustedRoot string
Owner *Owner
}
func (s Store) ReadFile(path string) ([]byte, error) {
directory, base, err := s.openParent(path, false)
if err != nil {
return nil, err
}
defer directory.Close()
file, err := OpenAt(directory, base, unix.O_RDONLY|unix.O_NONBLOCK, 0)
if err != nil {
return nil, err
}
defer file.Close()
if err := regular(file); err != nil {
return nil, err
}
return io.ReadAll(file)
}
func (s Store) FileMode(path string) (os.FileMode, error) {
directory, base, err := s.openParent(path, false)
if err != nil {
return 0, err
}
defer directory.Close()
file, err := OpenAt(directory, base, unix.O_RDONLY|unix.O_NONBLOCK, 0)
if err != nil {
return 0, err
}
defer file.Close()
if err := regular(file); err != nil {
return 0, err
}
info, err := file.Stat()
if err != nil {
return 0, err
}
return info.Mode().Perm(), nil
}
func (s Store) AtomicWrite(path string, data []byte, mode os.FileMode) error {
directory, base, err := s.openParent(path, true)
if err != nil {
return err
}
defer directory.Close()
if existing, openErr := OpenAt(directory, base, unix.O_RDONLY|unix.O_NONBLOCK, 0); openErr == nil {
err = regular(existing)
_ = existing.Close()
if err != nil {
return err
}
} else if !errors.Is(openErr, fs.ErrNotExist) {
return fmt.Errorf("inspect destination without following links: %w", openErr)
}
temporary, temporaryBase, err := CreateTemporaryAt(directory, ".proxyctl-")
if err != nil {
return err
}
committed := false
defer func() {
if !committed {
_ = unix.Unlinkat(int(directory.Fd()), temporaryBase, 0)
}
}()
if s.Owner != nil && (s.Owner.UID != os.Geteuid() || s.Owner.GID != os.Getegid()) {
if err := temporary.Chown(s.Owner.UID, s.Owner.GID); err != nil {
_ = temporary.Close()
return err
}
}
if err := temporary.Chmod(mode); err != nil {
_ = temporary.Close()
return err
}
if _, err := temporary.Write(data); err != nil {
_ = temporary.Close()
return err
}
if err := temporary.Sync(); err != nil {
_ = temporary.Close()
return err
}
if err := temporary.Close(); err != nil {
return err
}
if err := unix.Renameat(int(directory.Fd()), temporaryBase, int(directory.Fd()), base); err != nil {
return err
}
committed = true
return directory.Sync()
}
func (s Store) Remove(path string) error {
directory, base, err := s.openParent(path, false)
if err != nil {
return err
}
defer directory.Close()
file, err := OpenAt(directory, base, unix.O_RDONLY|unix.O_NONBLOCK, 0)
if err != nil {
return err
}
if err := regular(file); err != nil {
_ = file.Close()
return err
}
_ = file.Close()
if err := unix.Unlinkat(int(directory.Fd()), base, 0); err != nil {
return err
}
return directory.Sync()
}
func (s Store) openParent(path string, create bool) (*os.File, string, error) {
path = filepath.Clean(path)
root := filepath.Clean(s.TrustedRoot)
if !filepath.IsAbs(path) || !filepath.IsAbs(root) || !within(path, root) || path == root {
return nil, "", errors.New("secure file path escapes its trusted root")
}
parent, base := filepath.Dir(path), filepath.Base(path)
current, err := OpenNoFollow(string(filepath.Separator), unix.O_RDONLY|unix.O_DIRECTORY, 0)
if err != nil {
return nil, "", err
}
currentPath := string(filepath.Separator)
for _, component := range strings.Split(strings.TrimPrefix(parent, string(filepath.Separator)), string(filepath.Separator)) {
if component == "" {
continue
}
nextPath := filepath.Join(currentPath, component)
next, openErr := OpenAt(current, component, unix.O_RDONLY|unix.O_DIRECTORY, 0)
if errors.Is(openErr, fs.ErrNotExist) && create && nextPath != root && within(nextPath, root) {
created := false
if mkdirErr := unix.Mkdirat(int(current.Fd()), component, 0o700); mkdirErr != nil {
if !errors.Is(mkdirErr, fs.ErrExist) {
_ = current.Close()
return nil, "", mkdirErr
}
} else {
created = true
}
next, openErr = OpenAt(current, component, unix.O_RDONLY|unix.O_DIRECTORY, 0)
if openErr == nil && created {
if s.Owner != nil && (s.Owner.UID != os.Geteuid() || s.Owner.GID != os.Getegid()) {
if err := next.Chown(s.Owner.UID, s.Owner.GID); err != nil {
_ = next.Close()
_ = current.Close()
return nil, "", err
}
}
if err := next.Chmod(0o700); err != nil {
_ = next.Close()
_ = current.Close()
return nil, "", err
}
}
}
_ = current.Close()
if openErr != nil {
return nil, "", fmt.Errorf("open ancestor %q without following links: %w", nextPath, openErr)
}
current, currentPath = next, nextPath
}
return current, base, nil
}
func regular(file *os.File) error {
info, err := file.Stat()
if err != nil {
return err
}
if !info.Mode().IsRegular() {
return errors.New("destination is not a regular file")
}
return nil
}
func within(path, root string) bool {
relative, err := filepath.Rel(root, path)
return err == nil && relative != ".." && !strings.HasPrefix(relative, ".."+string(filepath.Separator))
}