security.go
  1  package fileserver
  2  
  3  import (
  4  	"errors"
  5  	"fmt"
  6  	"os"
  7  	"path/filepath"
  8  	"regexp"
  9  	"strings"
 10  
 11  	"github.com/easyshell-org/easyshell/easyshell-agent/internal/config"
 12  )
 13  
 14  var (
 15  	ErrPathTraversal    = errors.New("path traversal detected")
 16  	ErrAccessDenied     = errors.New("access denied by security rules")
 17  	ErrSymlinkTraversal = errors.New("symlink traversal detected")
 18  	ErrReadOnly         = errors.New("file system is read-only")
 19  	ErrFileTooLarge     = errors.New("file exceeds maximum size")
 20  )
 21  
 22  var DefaultBlacklist = []string{
 23  	`^/etc/shadow$`,
 24  	`^/etc/gshadow$`,
 25  	`^/etc/sudoers$`,
 26  	`^/etc/sudoers\.d/`,
 27  	`^/etc/ssh/ssh_host_`,
 28  	`/\.ssh/id_`,
 29  	`/\.ssh/authorized_keys$`,
 30  	`/\.gnupg/`,
 31  	`/\.aws/credentials$`,
 32  	`/\.aws/config$`,
 33  	`/\.kube/config$`,
 34  	`/\.docker/config\.json$`,
 35  	`/\.[a-z]*_history$`,
 36  	`/\.env$`,
 37  	`/\.env\.`,
 38  	`/secrets?\.(ya?ml|json|toml|properties|conf)$`,
 39  	`/credentials?\.(ya?ml|json|toml|properties|conf)$`,
 40  	`/\.git/config$`,
 41  	`/\.gitconfig$`,
 42  	`/\.netrc$`,
 43  	`/\.pgpass$`,
 44  	`/private[_-]?key`,
 45  	`\.pem$`,
 46  	`\.key$`,
 47  }
 48  
 49  type SecurityConfig struct {
 50  	Enabled          bool
 51  	RootPath         string
 52  	ReadOnly         bool
 53  	MaxFileSize      int64
 54  	MaxListEntries   int
 55  	BlacklistRegexp  []*regexp.Regexp
 56  	WhitelistPaths   []string
 57  }
 58  
 59  func NewSecurityConfig(cfg *config.FileConfig) *SecurityConfig {
 60  	sc := &SecurityConfig{
 61  		Enabled:        cfg.Enabled,
 62  		RootPath:       filepath.Clean(cfg.Root),
 63  		ReadOnly:       cfg.ReadOnly,
 64  		MaxFileSize:    int64(cfg.MaxSizeMB) * 1024 * 1024,
 65  		MaxListEntries: cfg.MaxListEntries,
 66  		WhitelistPaths: cfg.Whitelist,
 67  	}
 68  
 69  	for _, pattern := range DefaultBlacklist {
 70  		if re, err := regexp.Compile(pattern); err == nil {
 71  			sc.BlacklistRegexp = append(sc.BlacklistRegexp, re)
 72  		}
 73  	}
 74  	for _, pattern := range cfg.Blacklist {
 75  		if re, err := regexp.Compile(pattern); err == nil {
 76  			sc.BlacklistRegexp = append(sc.BlacklistRegexp, re)
 77  		}
 78  	}
 79  
 80  	return sc
 81  }
 82  
 83  func (sc *SecurityConfig) ValidatePath(requestedPath string) (string, error) {
 84  	cleaned := filepath.Clean(requestedPath)
 85  	absPath := filepath.Clean(filepath.Join(sc.RootPath, cleaned))
 86  
 87  	rootClean := filepath.Clean(sc.RootPath)
 88  	// Special case: when root is "/", all absolute paths are valid under root
 89  	if rootClean == "/" {
 90  		if !filepath.IsAbs(absPath) {
 91  			return "", ErrPathTraversal
 92  		}
 93  	} else {
 94  		if !strings.HasPrefix(absPath, rootClean+string(os.PathSeparator)) && absPath != rootClean {
 95  			return "", ErrPathTraversal
 96  		}
 97  	}
 98  
 99  	if _, err := os.Lstat(absPath); err == nil {
100  		evalPath, err := filepath.EvalSymlinks(absPath)
101  		if err == nil {
102  			evalClean := filepath.Clean(evalPath)
103  			if rootClean != "/" {
104  				if !strings.HasPrefix(evalClean, rootClean+string(os.PathSeparator)) && evalClean != rootClean {
105  					return "", ErrSymlinkTraversal
106  				}
107  			}
108  		}
109  	}
110  
111  	for _, re := range sc.BlacklistRegexp {
112  		if re.MatchString(absPath) {
113  			return "", ErrAccessDenied
114  		}
115  	}
116  
117  	if len(sc.WhitelistPaths) > 0 {
118  		allowed := false
119  		for _, w := range sc.WhitelistPaths {
120  			wClean := filepath.Clean(w)
121  			if strings.HasPrefix(absPath, wClean+string(os.PathSeparator)) || absPath == wClean {
122  				allowed = true
123  				break
124  			}
125  		}
126  		if !allowed {
127  			return "", ErrAccessDenied
128  		}
129  	}
130  
131  	return absPath, nil
132  }
133  
134  func (sc *SecurityConfig) ValidateWrite(requestedPath string) (string, error) {
135  	if sc.ReadOnly {
136  		return "", ErrReadOnly
137  	}
138  	return sc.ValidatePath(requestedPath)
139  }
140  
141  func (sc *SecurityConfig) ValidateFileSize(size int64) error {
142  	if size > sc.MaxFileSize {
143  		return fmt.Errorf("%w: %d > %d", ErrFileTooLarge, size, sc.MaxFileSize)
144  	}
145  	return nil
146  }