Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# Changelog

## v1.13.0
### Fixed
* Empty folders in uploaded artifacts are now preserved ([#325](https://github.com/pterodactyl/wings/pull/325))
* Directories created via the panel are no longer owned by `root:root` ([#328](https://github.com/pterodactyl/wings/pull/328))
* Fixed the length check when accepting SFTP connections
* Properly close filesystem copies, compression streams, and Docker responses to avoid resource leaks
* The `file` config parser no longer creates files that do not exist yet

### Added
* Only set the container block IO weight when the host supports `io.weight` ([#324](https://github.com/pterodactyl/wings/pull/324))
* Reasonable 64 MB limits for config file parsing and line scanning

## v1.12.3
### Fixed
* Support properly restricting configuration in egg templating

## v1.12.2
### Fixed
* Fixes a bug where `fs.Chmod` would change the symlink target possibly allowing a malicious user to modify files outside their home directory.
Expand Down
2 changes: 2 additions & 0 deletions environment/docker/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,10 @@ func (e *Environment) ContainerInspect(ctx context.Context) (types.ContainerJSON
if res == nil {
return st, errdefs.Unknown(err)
}
_ = res.Body.Close()
return st, errdefs.FromStatusCode(err, res.StatusCode)
}
defer res.Body.Close()

body, err := io.ReadAll(res.Body)
if err != nil {
Expand Down
15 changes: 8 additions & 7 deletions internal/ufs/filesystem.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,14 @@ type Filesystem interface {
Mkdir(name string, perm FileMode) error

// MkdirAll creates a directory named path, along with any necessary
// parents, and returns nil, or else returns an error.
//
// The permission bits perm (before umask) are used for all
// directories that MkdirAll creates.
// If path is already a directory, MkdirAll does nothing
// and returns nil.
MkdirAll(path string, perm FileMode) error
// parents, and returns the directories it created, or else returns an
// error.
//
// The returned directories are ordered from shallowest to deepest. The
// permission bits perm (before umask) are used for all directories that
// MkdirAll creates. If path is already a directory, MkdirAll does nothing
// and returns no created directories.
MkdirAll(path string, perm FileMode) ([]string, error)

// Open opens the named file for reading.
//
Expand Down
18 changes: 9 additions & 9 deletions internal/ufs/fs_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,17 +207,17 @@ func (fs *UnixFS) mkdirat(op string, dirfd int, name string, mode FileMode) erro
}

// MkdirAll creates a directory named path, along with any necessary
// parents, and returns nil, or else returns an error.
// parents, and returns the directories it created, or else returns an error.
//
// The permission bits perm (before umask) are used for all
// directories that MkdirAll creates.
// If path is already a directory, MkdirAll does nothing
// and returns nil.
func (fs *UnixFS) MkdirAll(name string, mode FileMode) error {
// The returned directories are ordered from shallowest to deepest. The
// permission bits perm (before umask) are used for all directories that
// MkdirAll creates. If path is already a directory, MkdirAll does nothing and
// returns no created directories.
func (fs *UnixFS) MkdirAll(name string, mode FileMode) ([]string, error) {
// Ensure name is somewhat clean before continuing.
name, err := fs.unsafePath(name)
if err != nil {
return err
return nil, err
}
return fs.mkdirAll(name, mode)
}
Expand Down Expand Up @@ -471,7 +471,7 @@ func (fs *UnixFS) Rename(oldpath, newpath string) error {
if !errors.As(err, &pathErr) {
return err
}
if err := fs.MkdirAll(pathErr.Path, 0o755); err != nil {
if _, err := fs.MkdirAll(pathErr.Path, 0o755); err != nil {
return err
}
newdirfd, newname, closeFd2, err = fs.safePath(newpath)
Expand Down Expand Up @@ -623,7 +623,7 @@ func (fs *UnixFS) TouchPath(path string) (int, string, func(), error, bool) {
if !errors.As(err, &pathErr) {
return dirfd, name, closeFd, err, false
}
if err := fs.MkdirAll(pathErr.Path, 0o755); err != nil {
if _, err := fs.MkdirAll(pathErr.Path, 0o755); err != nil {
return dirfd, name, closeFd, err, false
}

Expand Down
61 changes: 54 additions & 7 deletions internal/ufs/fs_unix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ func TestUnixFS(t *testing.T) {
}

// Create multiple nested directories.
if err := fs.MkdirAll("ima_directory/ima_directory/ima_directory/ima_directory", 0o755); err != nil {
if _, err := fs.MkdirAll("ima_directory/ima_directory/ima_directory/ima_directory", 0o755); err != nil {
t.Error(err)
return
}
Expand All @@ -174,7 +174,7 @@ func TestUnixFS(t *testing.T) {
}

// Test creating a directory under a symlink with a pre-existing directory.
if err := fs.MkdirAll("ima_bad_link/ima_directory/ima_bad_directory/ima_bad_directory", 0o755); err == nil {
if _, err := fs.MkdirAll("ima_bad_link/ima_directory/ima_bad_directory/ima_bad_directory", 0o755); err == nil {
t.Error("expected an error")
return
}
Expand Down Expand Up @@ -324,12 +324,59 @@ func TestUnixFS_MkdirAll(t *testing.T) {
}
defer fs.Cleanup()

if err := fs.MkdirAll("/a/bunch/of/directories", 0o755); err != nil {
t.Error(err)
return
}
t.Run("creates and reports every missing directory", func(t *testing.T) {
created, err := fs.MkdirAll("/a/bunch/of/directories", 0o755)
if err != nil {
t.Fatal(err)
}

want := []string{"a", "a/bunch", "a/bunch/of", "a/bunch/of/directories"}
if !slices.Equal(created, want) {
t.Errorf("created = %v, want %v", created, want)
}

// TODO: stat sanity check
// Sanity check that everything we reported actually exists on disk.
for _, dir := range want {
st, err := os.Lstat(filepath.Join(fs.Root, dir))
if err != nil {
t.Errorf("Lstat %q: %v", dir, err)
continue
}
if !st.IsDir() {
t.Errorf("%q is not a directory", dir)
}
}
})

t.Run("only reports the directories it creates", func(t *testing.T) {
if _, err := fs.MkdirAll("partial/exists", 0o755); err != nil {
t.Fatalf("seeding directories: %v", err)
}

created, err := fs.MkdirAll("partial/exists/and/more", 0o755)
if err != nil {
t.Fatal(err)
}

want := []string{"partial/exists/and", "partial/exists/and/more"}
if !slices.Equal(created, want) {
t.Errorf("created = %v, want %v", created, want)
}
})

t.Run("reports nothing when the directory already exists", func(t *testing.T) {
if _, err := fs.MkdirAll("already/here", 0o755); err != nil {
t.Fatalf("seeding directories: %v", err)
}

created, err := fs.MkdirAll("already/here", 0o755)
if err != nil {
t.Fatal(err)
}
if len(created) != 0 {
t.Errorf("created = %v, want no directories", created)
}
})
}

func TestUnixFS_Open(t *testing.T) {
Expand Down
23 changes: 14 additions & 9 deletions internal/ufs/mkdir_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@
package ufs

// mkdirAll is a recursive Mkdir implementation that properly handles symlinks.
func (fs *UnixFS) mkdirAll(name string, mode FileMode) error {
//
// It returns the directories it created, ordered from shallowest to deepest, so
// callers can act on exactly the paths that were new (for example to change
// their ownership). Directories that already existed are not included.
func (fs *UnixFS) mkdirAll(name string, mode FileMode) ([]string, error) {
// Fast path: if we can tell whether path is a directory or file, stop with success or error.
dir, err := fs.Lstat(name)
if err == nil {
Expand All @@ -20,13 +24,13 @@ func (fs *UnixFS) mkdirAll(name string, mode FileMode) error {
// to check instead.
dir, err = fs.Stat(name)
if err != nil {
return err
return nil, err
}
}
if dir.IsDir() {
return nil
return nil, nil
}
return &PathError{Op: "mkdir", Path: name, Err: ErrNotDirectory}
return nil, &PathError{Op: "mkdir", Path: name, Err: ErrNotDirectory}
}

// Slow path: make sure parent exists and then call Mkdir for path.
Expand All @@ -40,11 +44,12 @@ func (fs *UnixFS) mkdirAll(name string, mode FileMode) error {
j--
}

var created []string
if j > 1 {
// Create parent.
err = fs.mkdirAll(name[:j-1], mode)
created, err = fs.mkdirAll(name[:j-1], mode)
if err != nil {
return err
return created, err
}
}

Expand All @@ -55,9 +60,9 @@ func (fs *UnixFS) mkdirAll(name string, mode FileMode) error {
// double-checking that directory doesn't exist.
dir, err1 := fs.Lstat(name)
if err1 == nil && dir.IsDir() {
return nil
return created, nil
}
return err
return created, err
}
return nil
return append(created, name), nil
}
32 changes: 26 additions & 6 deletions parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ const (
Xml = "xml"
)

// maxTextScanTokenSize bounds how large a single line the "file" parser will buffer.
const maxTextScanTokenSize = 64 * 1024 * 1024

// maxConfigFileSize caps how large a configuration file we'll attempt to parse.
const maxConfigFileSize = 64 * 1024 * 1024

type ReplaceValue struct {
value []byte
valueType jsonparser.ValueType
Expand Down Expand Up @@ -209,6 +215,16 @@ func newTemplatableConfig(c *config.Configuration) templatableConfig {
func (f *ConfigurationFile) Parse(file ufs.File) error {
// log.WithField("path", path).WithField("parser", f.Parser.String()).Debug("parsing server configuration file")

// Refuse to parse files larger than the cap. Every parser below buffers the
// whole file in memory, and the contents are untrusted server-owned input, so
// this guards the daemon against being OOM'd by an oversized config. The
// server is still free to boot with the file as-is; we just don't rewrite it.
if info, err := file.Stat(); err != nil {
return err
} else if info.Size() > maxConfigFileSize {
return errors.Errorf("parser: refusing to parse configuration file %q: size %d exceeds limit of %d bytes", file.Name(), info.Size(), maxConfigFileSize)
}

if mb, err := json.Marshal(newTemplatableConfig(config.Get())); err != nil {
return err
} else {
Expand Down Expand Up @@ -237,7 +253,7 @@ func (f *ConfigurationFile) Parse(file ufs.File) error {
// Parses an xml file.
func (f *ConfigurationFile) parseXmlFile(file ufs.File) error {
doc := etree.NewDocument()
if _, err := doc.ReadFrom(file); err != nil {
if _, err := doc.ReadFrom(io.LimitReader(file, maxConfigFileSize)); err != nil {
return err
}

Expand Down Expand Up @@ -316,7 +332,7 @@ func (f *ConfigurationFile) parseXmlFile(file ufs.File) error {
// Parses an ini file.
func (f *ConfigurationFile) parseIniFile(file ufs.File) error {
// Wrap the file in a NopCloser so the ini package doesn't close the file.
cfg, err := ini.Load(io.NopCloser(file))
cfg, err := ini.Load(io.NopCloser(io.LimitReader(file, maxConfigFileSize)))
if err != nil {
return err
}
Expand Down Expand Up @@ -396,7 +412,7 @@ func (f *ConfigurationFile) parseIniFile(file ufs.File) error {
// value is set regardless in the file. See the commentary in parseYamlFile for more details
// about what is happening during this process.
func (f *ConfigurationFile) parseJsonFile(file ufs.File) error {
b, err := io.ReadAll(file)
b, err := io.ReadAll(io.LimitReader(file, maxConfigFileSize))
if err != nil {
return err
}
Expand All @@ -423,7 +439,7 @@ func (f *ConfigurationFile) parseJsonFile(file ufs.File) error {
// Parses a yaml file and updates any matching key/value pairs before persisting
// it back to the disk.
func (f *ConfigurationFile) parseYamlFile(file ufs.File) error {
b, err := io.ReadAll(file)
b, err := io.ReadAll(io.LimitReader(file, maxConfigFileSize))
if err != nil {
return err
}
Expand Down Expand Up @@ -473,7 +489,8 @@ func (f *ConfigurationFile) parseYamlFile(file ufs.File) error {
// than this function where possible.
func (f *ConfigurationFile) parseTextFile(file ufs.File) error {
b := bytes.NewBuffer(nil)
s := bufio.NewScanner(file)
s := bufio.NewScanner(io.LimitReader(file, maxConfigFileSize))
s.Buffer(make([]byte, 0, 64*1024), maxTextScanTokenSize)
var replaced bool
for s.Scan() {
line := s.Bytes()
Expand All @@ -492,6 +509,9 @@ func (f *ConfigurationFile) parseTextFile(file ufs.File) error {
}
b.WriteByte('\n')
}
if err := s.Err(); err != nil {
return errors.Wrap(err, "parser: failed to scan text file for configuration update")
}

if _, err := file.Seek(0, io.SeekStart); err != nil {
return err
Expand Down Expand Up @@ -534,7 +554,7 @@ func (f *ConfigurationFile) parseTextFile(file ufs.File) error {
// @see https://github.com/pterodactyl/panel/issues/2308 (original)
// @see https://github.com/pterodactyl/panel/issues/3009 ("bug" introduced as result)
func (f *ConfigurationFile) parsePropertiesFile(file ufs.File) error {
b, err := io.ReadAll(file)
b, err := io.ReadAll(io.LimitReader(file, maxConfigFileSize))
if err != nil {
return err
}
Expand Down
10 changes: 9 additions & 1 deletion server/backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -675,9 +675,17 @@ func (s *Server) RestoreBackupWithContext(ctx context.Context, b backup.BackupIn
// Handle directories and files differently
if info.IsDir() {
// For directories, create the directory structure using the underlying UnixFS
if err := s.Filesystem().UnixFS().MkdirAll(file, ufs.FileMode(info.Mode())); err != nil {
created, err := s.Filesystem().UnixFS().MkdirAll(file, ufs.FileMode(info.Mode()))
if err != nil {
return err
}
// Chown every directory we just created so restored directories are
// owned by the server user instead of the user Wings runs as.
for _, dir := range created {
if err := s.Filesystem().Chown(dir); err != nil {
return err
}
}
// Set directory timestamps
atime := info.ModTime()
return s.Filesystem().Chtimes(file, atime, atime)
Expand Down
9 changes: 9 additions & 0 deletions server/config_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package server
import (
"runtime"

"emperror.dev/errors"
"github.com/gammazero/workerpool"

"github.com/Rene-Roscher/wings/internal/ufs"
"github.com/Rene-Roscher/wings/parser"
)

// UpdateConfigurationFiles updates all the defined configuration files for
Expand All @@ -20,6 +22,13 @@ func (s *Server) UpdateConfigurationFiles() {
f := cf

pool.Submit(func() {
if f.Parser == parser.File {
if _, err := s.Filesystem().UnixFS().Stat(f.FileName); errors.Is(err, ufs.ErrNotExist) {
s.Log().WithField("file_name", f.FileName).Debug("skipping text configuration file that does not exist yet")
return
}
}

file, err := s.Filesystem().UnixFS().Touch(f.FileName, ufs.O_RDWR|ufs.O_CREATE, 0o644)
if err != nil {
s.Log().WithField("file_name", f.FileName).WithField("error", err).Error("failed to open file for configuration")
Expand Down
Loading