Add base idea for denying write access to certain files; ref pterodactyl/panel#569

This commit is contained in:
Dane Everitt 2021-01-10 16:33:39 -08:00
parent 3459c25be0
commit 2c1b211280
No known key found for this signature in database
GPG Key ID: EEA66103B3D71F53
7 changed files with 61 additions and 14 deletions

View File

@ -94,8 +94,7 @@ func putServerRenameFiles(c *gin.Context) {
return 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. // Loop over the array of files passed in and perform the move or rename action against each.
for _, p := range data.Files { for _, p := range data.Files {
pf := path.Join(data.Root, p.From) pf := path.Join(data.Root, p.From)
@ -106,16 +105,20 @@ func putServerRenameFiles(c *gin.Context) {
case <-ctx.Done(): case <-ctx.Done():
return ctx.Err() return ctx.Err()
default: 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. // Return nil if the error is an is not exists.
// NOTE: os.IsNotExist() does not work if the error is wrapped. // NOTE: os.IsNotExist() does not work if the error is wrapped.
if errors.Is(err, os.ErrNotExist) { if errors.Is(err, os.ErrNotExist) {
return nil return nil
} }
return err return err
} }
return nil return nil
} }
}) })
@ -148,6 +151,10 @@ func postServerCopyFile(c *gin.Context) {
return 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 { if err := s.Filesystem().Copy(data.Location); err != nil {
NewServerError(err, s).AbortFilesystemError(c) NewServerError(err, s).AbortFilesystemError(c)
return return
@ -208,6 +215,10 @@ func postServerWriteFile(c *gin.Context) {
f := c.Query("file") f := c.Query("file")
f = "/" + strings.TrimLeft(f, "/") 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 err := s.Filesystem().Writefile(f, c.Request.Body); err != nil {
if filesystem.IsErrorCode(err, filesystem.ErrCodeIsDirectory) { if filesystem.IsErrorCode(err, filesystem.ErrCodeIsDirectory) {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
@ -557,6 +568,9 @@ func handleFileUpload(p string, s *server.Server, header *multipart.FileHeader)
} }
defer file.Close() defer file.Close()
if err := s.Filesystem().IsIgnored(p); err != nil {
return err
}
if err := s.Filesystem().Writefile(p, file); err != nil { if err := s.Filesystem().Writefile(p, file); err != nil {
return err return err
} }

View File

@ -6,6 +6,16 @@ import (
"github.com/pterodactyl/wings/environment" "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 { type Configuration struct {
mu sync.RWMutex mu sync.RWMutex
@ -34,6 +44,7 @@ type Configuration struct {
CrashDetectionEnabled bool `default:"true" json:"enabled" yaml:"enabled"` CrashDetectionEnabled bool `default:"true" json:"enabled" yaml:"enabled"`
Mounts []Mount `json:"mounts"` Mounts []Mount `json:"mounts"`
Resources ResourceUsage `json:"resources"` Resources ResourceUsage `json:"resources"`
Egg EggConfiguration `json:"egg,omitempty"`
Container struct { Container struct {
// Defines the Docker image that will be used for this server // Defines the Docker image that will be used for this server

View File

@ -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())) 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)) p := filepath.Join(dir, name)
if err != nil { if err := fs.IsIgnored(p); err != nil {
return errors.WithMessage(err, "failed to generate a safe path to server file") return err
} }
return errors.WithMessage(fs.Writefile(p, f), "could not extract file from archive") return errors.WithMessage(fs.Writefile(p, f), "could not extract file from archive")
}) })
if err != nil { if err != nil {

View File

@ -1,11 +1,12 @@
package filesystem package filesystem
import ( import (
"emperror.dev/errors"
"fmt" "fmt"
"github.com/apex/log"
"os" "os"
"path/filepath" "path/filepath"
"emperror.dev/errors"
"github.com/apex/log"
) )
type ErrorCode string type ErrorCode string
@ -15,6 +16,7 @@ const (
ErrCodeDiskSpace ErrorCode = "E_NODISK" ErrCodeDiskSpace ErrorCode = "E_NODISK"
ErrCodeUnknownArchive ErrorCode = "E_UNKNFMT" ErrCodeUnknownArchive ErrorCode = "E_UNKNFMT"
ErrCodePathResolution ErrorCode = "E_BADPATH" ErrCodePathResolution ErrorCode = "E_BADPATH"
ErrCodeDenylistFile ErrorCode = "E_DENYLIST"
) )
type Error struct { type Error struct {
@ -32,6 +34,8 @@ func (e *Error) Error() string {
return "filesystem: not enough disk space" return "filesystem: not enough disk space"
case ErrCodeUnknownArchive: case ErrCodeUnknownArchive:
return "filesystem: unknown archive format" return "filesystem: unknown archive format"
case ErrCodeDenylistFile:
return "filesystem: file access prohibited: denylist"
case ErrCodePathResolution: case ErrCodePathResolution:
r := e.resolved r := e.resolved
if r == "" { if r == "" {

View File

@ -18,6 +18,7 @@ import (
"github.com/karrick/godirwalk" "github.com/karrick/godirwalk"
"github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/system" "github.com/pterodactyl/wings/system"
ignore "github.com/sabhiram/go-gitignore"
) )
type Filesystem struct { type Filesystem struct {
@ -26,6 +27,7 @@ type Filesystem struct {
lookupInProgress *system.AtomicBool lookupInProgress *system.AtomicBool
diskUsed int64 diskUsed int64
diskCheckInterval time.Duration diskCheckInterval time.Duration
denylist *ignore.GitIgnore
// The maximum amount of disk space (in bytes) that this Filesystem instance can use. // The maximum amount of disk space (in bytes) that this Filesystem instance can use.
diskLimit int64 diskLimit int64
@ -37,13 +39,14 @@ type Filesystem struct {
} }
// Creates a new Filesystem instance for a given server. // 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{ return &Filesystem{
root: root, root: root,
diskLimit: size, diskLimit: size,
diskCheckInterval: time.Duration(config.Get().System.DiskCheckInterval), diskCheckInterval: time.Duration(config.Get().System.DiskCheckInterval),
lastLookupTime: &usageLookupTime{}, lastLookupTime: &usageLookupTime{},
lookupInProgress: system.NewAtomicBool(false), lookupInProgress: system.NewAtomicBool(false),
denylist: ignore.CompileIgnoreLines(denylist...),
} }
} }

View File

@ -2,13 +2,29 @@ package filesystem
import ( import (
"context" "context"
"golang.org/x/sync/errgroup"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"sync" "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 // 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 // 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. // path it is returned. If they managed to "escape" an error will be returned.

View File

@ -96,7 +96,7 @@ func FromConfiguration(data api.ServerConfigurationResponse) (*Server, error) {
} }
s.Archiver = Archiver{Server: s} 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 // 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 // this logic in. When we're ready to support other environment we'll need to make