Add base idea for denying write access to certain files; ref pterodactyl/panel#569
This commit is contained in:
parent
3459c25be0
commit
2c1b211280
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 == "" {
|
||||||
|
|
|
@ -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...),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue
Block a user