From 2c1b211280b872db28b59640087ea0a15645aba5 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 10 Jan 2021 16:33:39 -0800 Subject: [PATCH] Add base idea for denying write access to certain files; ref pterodactyl/panel#569 --- router/router_server_files.go | 24 +++++++++++++++++++----- server/configuration.go | 11 +++++++++++ server/filesystem/decompress.go | 7 +++---- server/filesystem/errors.go | 8 ++++++-- server/filesystem/filesystem.go | 5 ++++- server/filesystem/path.go | 18 +++++++++++++++++- server/loader.go | 2 +- 7 files changed, 61 insertions(+), 14 deletions(-) diff --git a/router/router_server_files.go b/router/router_server_files.go index d0e6da5..bee077d 100644 --- a/router/router_server_files.go +++ b/router/router_server_files.go @@ -94,8 +94,7 @@ func putServerRenameFiles(c *gin.Context) { return } - g, ctx := errgroup.WithContext(context.Background()) - + g, ctx := errgroup.WithContext(c.Request.Context()) // Loop over the array of files passed in and perform the move or rename action against each. for _, p := range data.Files { pf := path.Join(data.Root, p.From) @@ -106,16 +105,20 @@ func putServerRenameFiles(c *gin.Context) { case <-ctx.Done(): return ctx.Err() default: - if err := s.Filesystem().Rename(pf, pt); err != nil { + fs := s.Filesystem() + // Ignore renames on a file that is on the denylist (both as the rename from or + // the rename to value). + if err := fs.IsIgnored(pf, pt); err != nil { + return err + } + if err := fs.Rename(pf, pt); err != nil { // Return nil if the error is an is not exists. // NOTE: os.IsNotExist() does not work if the error is wrapped. if errors.Is(err, os.ErrNotExist) { return nil } - return err } - return nil } }) @@ -148,6 +151,10 @@ func postServerCopyFile(c *gin.Context) { return } + if err := s.Filesystem().IsIgnored(data.Location); err != nil { + NewServerError(err, s).Abort(c) + return + } if err := s.Filesystem().Copy(data.Location); err != nil { NewServerError(err, s).AbortFilesystemError(c) return @@ -208,6 +215,10 @@ func postServerWriteFile(c *gin.Context) { f := c.Query("file") f = "/" + strings.TrimLeft(f, "/") + if err := s.Filesystem().IsIgnored(f); err != nil { + NewServerError(err, s).Abort(c) + return + } if err := s.Filesystem().Writefile(f, c.Request.Body); err != nil { if filesystem.IsErrorCode(err, filesystem.ErrCodeIsDirectory) { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{ @@ -557,6 +568,9 @@ func handleFileUpload(p string, s *server.Server, header *multipart.FileHeader) } defer file.Close() + if err := s.Filesystem().IsIgnored(p); err != nil { + return err + } if err := s.Filesystem().Writefile(p, file); err != nil { return err } diff --git a/server/configuration.go b/server/configuration.go index 5013650..b7f16b7 100644 --- a/server/configuration.go +++ b/server/configuration.go @@ -6,6 +6,16 @@ import ( "github.com/pterodactyl/wings/environment" ) +type EggConfiguration struct { + // The internal UUID of the Egg on the Panel. + ID string + + // Maintains a list of files that are blacklisted for opening/editing/downloading + // or basically any type of access on the server by any user. This is NOT the same + // as a per-user denylist, this is defined at the Egg level. + FileDenylist []string `json:"file_denylist"` +} + type Configuration struct { mu sync.RWMutex @@ -34,6 +44,7 @@ type Configuration struct { CrashDetectionEnabled bool `default:"true" json:"enabled" yaml:"enabled"` Mounts []Mount `json:"mounts"` Resources ResourceUsage `json:"resources"` + Egg EggConfiguration `json:"egg,omitempty"` Container struct { // Defines the Docker image that will be used for this server diff --git a/server/filesystem/decompress.go b/server/filesystem/decompress.go index 7ff4593..66d7901 100644 --- a/server/filesystem/decompress.go +++ b/server/filesystem/decompress.go @@ -91,11 +91,10 @@ func (fs *Filesystem) DecompressFile(dir string, file string) error { return errors.New(fmt.Sprintf("could not parse underlying data source with type %s", reflect.TypeOf(s).String())) } - p, err := fs.SafePath(filepath.Join(dir, name)) - if err != nil { - return errors.WithMessage(err, "failed to generate a safe path to server file") + p := filepath.Join(dir, name) + if err := fs.IsIgnored(p); err != nil { + return err } - return errors.WithMessage(fs.Writefile(p, f), "could not extract file from archive") }) if err != nil { diff --git a/server/filesystem/errors.go b/server/filesystem/errors.go index a5890bd..bb3f1af 100644 --- a/server/filesystem/errors.go +++ b/server/filesystem/errors.go @@ -1,11 +1,12 @@ package filesystem import ( - "emperror.dev/errors" "fmt" - "github.com/apex/log" "os" "path/filepath" + + "emperror.dev/errors" + "github.com/apex/log" ) type ErrorCode string @@ -15,6 +16,7 @@ const ( ErrCodeDiskSpace ErrorCode = "E_NODISK" ErrCodeUnknownArchive ErrorCode = "E_UNKNFMT" ErrCodePathResolution ErrorCode = "E_BADPATH" + ErrCodeDenylistFile ErrorCode = "E_DENYLIST" ) type Error struct { @@ -32,6 +34,8 @@ func (e *Error) Error() string { return "filesystem: not enough disk space" case ErrCodeUnknownArchive: return "filesystem: unknown archive format" + case ErrCodeDenylistFile: + return "filesystem: file access prohibited: denylist" case ErrCodePathResolution: r := e.resolved if r == "" { diff --git a/server/filesystem/filesystem.go b/server/filesystem/filesystem.go index a4a49de..4732a22 100644 --- a/server/filesystem/filesystem.go +++ b/server/filesystem/filesystem.go @@ -18,6 +18,7 @@ import ( "github.com/karrick/godirwalk" "github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/system" + ignore "github.com/sabhiram/go-gitignore" ) type Filesystem struct { @@ -26,6 +27,7 @@ type Filesystem struct { lookupInProgress *system.AtomicBool diskUsed int64 diskCheckInterval time.Duration + denylist *ignore.GitIgnore // The maximum amount of disk space (in bytes) that this Filesystem instance can use. diskLimit int64 @@ -37,13 +39,14 @@ type Filesystem struct { } // Creates a new Filesystem instance for a given server. -func New(root string, size int64) *Filesystem { +func New(root string, size int64, denylist []string) *Filesystem { return &Filesystem{ root: root, diskLimit: size, diskCheckInterval: time.Duration(config.Get().System.DiskCheckInterval), lastLookupTime: &usageLookupTime{}, lookupInProgress: system.NewAtomicBool(false), + denylist: ignore.CompileIgnoreLines(denylist...), } } diff --git a/server/filesystem/path.go b/server/filesystem/path.go index 901c40b..e816854 100644 --- a/server/filesystem/path.go +++ b/server/filesystem/path.go @@ -2,13 +2,29 @@ package filesystem import ( "context" - "golang.org/x/sync/errgroup" "os" "path/filepath" "strings" "sync" + + "golang.org/x/sync/errgroup" ) +// Checks if the given file or path is in the server's file denylist. If so, an Error +// is returned, otherwise nil is returned. +func (fs *Filesystem) IsIgnored(paths ...string) error { + for _, p := range paths { + sp, err := fs.SafePath(p) + if err != nil { + return err + } + if fs.denylist.MatchesPath(sp) { + return &Error{code: ErrCodeDenylistFile, path: p, resolved: sp} + } + } + return nil +} + // Normalizes a directory being passed in to ensure the user is not able to escape // from their data directory. After normalization if the directory is still within their home // path it is returned. If they managed to "escape" an error will be returned. diff --git a/server/loader.go b/server/loader.go index c2d401e..1c0e2b8 100644 --- a/server/loader.go +++ b/server/loader.go @@ -96,7 +96,7 @@ func FromConfiguration(data api.ServerConfigurationResponse) (*Server, error) { } s.Archiver = Archiver{Server: s} - s.fs = filesystem.New(filepath.Join(config.Get().System.Data, s.Id()), s.DiskSpace()) + s.fs = filesystem.New(filepath.Join(config.Get().System.Data, s.Id()), s.DiskSpace(), s.Config().Egg.FileDenylist) // Right now we only support a Docker based environment, so I'm going to hard code // this logic in. When we're ready to support other environment we'll need to make