Compare commits

...

7 Commits

Author SHA1 Message Date
Pterodactyl CI
c6da2387db bump version for release 2023-05-10 19:47:29 +00:00
Matthew Penner
a10a3ed475
Update CHANGELOG.md 2023-05-10 13:24:01 -06:00
Matthew Penner
c0aff69d2b
server(install): remove privileges from install container 2023-05-10 13:16:06 -06:00
Matthew Penner
fb65487ed8
Update CHANGELOG.md 2023-02-08 14:22:31 -07:00
Matthew Penner
dcbc59790d
server(filesystem): Delete tweaks 2023-02-08 14:22:26 -07:00
Matthew Penner
00451b38db
Update CHANGELOG.md 2023-02-07 19:13:54 -07:00
Matthew Penner
9f6548eaa8
server(filesystem): SafePath tweaks 2023-02-07 19:11:56 -07:00
7 changed files with 185 additions and 57 deletions

View File

@ -1,5 +1,17 @@
# Changelog # Changelog
## v1.7.5
### Fixed
* CVE-2023-32080
## v1.7.4
### Fixed
* CVE-2023-25168
## v1.7.3
### Fixed
* CVE-2023-25152
## v1.7.2 ## v1.7.2
### Fixed ### Fixed
* The S3 backup driver now supports Cloudflare R2 * The S3 backup driver now supports Cloudflare R2

View File

@ -159,7 +159,7 @@ func (fs *Filesystem) Writefile(p string, r io.Reader) error {
// Adjust the disk usage to account for the old size and the new size of the file. // Adjust the disk usage to account for the old size and the new size of the file.
fs.addDisk(sz - currentSize) fs.addDisk(sz - currentSize)
return fs.Chown(cleaned) return fs.unsafeChown(cleaned)
} }
// Creates a new directory (name) at a specified path (p) for the server. // Creates a new directory (name) at a specified path (p) for the server.
@ -217,7 +217,12 @@ func (fs *Filesystem) Chown(path string) error {
if err != nil { if err != nil {
return err return err
} }
return fs.unsafeChown(cleaned)
}
// unsafeChown chowns the given path, without checking if the path is safe. This should only be used
// when the path has already been checked.
func (fs *Filesystem) unsafeChown(path string) error {
if fs.isTest { if fs.isTest {
return nil return nil
} }
@ -226,19 +231,19 @@ func (fs *Filesystem) Chown(path string) error {
gid := config.Get().System.User.Gid gid := config.Get().System.User.Gid
// Start by just chowning the initial path that we received. // Start by just chowning the initial path that we received.
if err := os.Chown(cleaned, uid, gid); err != nil { if err := os.Chown(path, uid, gid); err != nil {
return errors.Wrap(err, "server/filesystem: chown: failed to chown path") return errors.Wrap(err, "server/filesystem: chown: failed to chown path")
} }
// If this is not a directory we can now return from the function, there is nothing // If this is not a directory we can now return from the function, there is nothing
// left that we need to do. // left that we need to do.
if st, err := os.Stat(cleaned); err != nil || !st.IsDir() { if st, err := os.Stat(path); err != nil || !st.IsDir() {
return nil return nil
} }
// If this was a directory, begin walking over its contents recursively and ensure that all // If this was a directory, begin walking over its contents recursively and ensure that all
// of the subfiles and directories get their permissions updated as well. // of the subfiles and directories get their permissions updated as well.
err = godirwalk.Walk(cleaned, &godirwalk.Options{ err := godirwalk.Walk(path, &godirwalk.Options{
Unsorted: true, Unsorted: true,
Callback: func(p string, e *godirwalk.Dirent) error { Callback: func(p string, e *godirwalk.Dirent) error {
// Do not attempt to chown a symlink. Go's os.Chown function will affect the symlink // Do not attempt to chown a symlink. Go's os.Chown function will affect the symlink
@ -255,7 +260,6 @@ func (fs *Filesystem) Chown(path string) error {
return os.Chown(p, uid, gid) return os.Chown(p, uid, gid)
}, },
}) })
return errors.Wrap(err, "server/filesystem: chown: failed to chown during walk function") return errors.Wrap(err, "server/filesystem: chown: failed to chown during walk function")
} }
@ -377,10 +381,9 @@ func (fs *Filesystem) TruncateRootDirectory() error {
// Delete removes a file or folder from the system. Prevents the user from // Delete removes a file or folder from the system. Prevents the user from
// accidentally (or maliciously) removing their root server data directory. // accidentally (or maliciously) removing their root server data directory.
func (fs *Filesystem) Delete(p string) error { func (fs *Filesystem) Delete(p string) error {
wg := sync.WaitGroup{}
// This is one of the few (only?) places in the codebase where we're explicitly not using // This is one of the few (only?) places in the codebase where we're explicitly not using
// the SafePath functionality when working with user provided input. If we did, you would // the SafePath functionality when working with user provided input. If we did, you would
// not be able to delete a file that is a symlink pointing to a location outside of the data // not be able to delete a file that is a symlink pointing to a location outside the data
// directory. // directory.
// //
// We also want to avoid resolving a symlink that points _within_ the data directory and thus // We also want to avoid resolving a symlink that points _within_ the data directory and thus
@ -397,25 +400,65 @@ func (fs *Filesystem) Delete(p string) error {
return errors.New("cannot delete root server directory") return errors.New("cannot delete root server directory")
} }
if st, err := os.Lstat(resolved); err != nil { st, err := os.Lstat(resolved)
if err != nil {
if !os.IsNotExist(err) { if !os.IsNotExist(err) {
fs.error(err).Warn("error while attempting to stat file before deletion") fs.error(err).Warn("error while attempting to stat file before deletion")
return err
} }
} else {
if !st.IsDir() { // The following logic is used to handle a case where a user attempts to
fs.addDisk(-st.Size()) // delete a file that does not exist through a directory symlink.
} else { // We don't want to reveal that the file does not exist, so we validate
wg.Add(1) // the path of the symlink and return a bad path error if it is invalid.
go func(wg *sync.WaitGroup, st os.FileInfo, resolved string) {
defer wg.Done() // The requested file or directory doesn't exist, so at this point we
if s, err := fs.DirectorySize(resolved); err == nil { // need to iterate up the path chain until we hit a directory that
fs.addDisk(-s) // _does_ exist and can be validated.
parts := strings.Split(filepath.Dir(resolved), "/")
// Range over all the path parts and form directory paths from the end
// moving up until we have a valid resolution, or we run out of paths to
// try.
for k := range parts {
try := strings.Join(parts[:(len(parts)-k)], "/")
if !fs.unsafeIsInDataDirectory(try) {
break
} }
}(&wg, st, resolved)
t, err := filepath.EvalSymlinks(try)
if err == nil {
if !fs.unsafeIsInDataDirectory(t) {
return NewBadPathResolution(p, t)
}
break
} }
} }
wg.Wait() // Always return early if the file does not exist.
return nil
}
// If the file is not a symlink, we need to check that it is not within a
// symlinked directory that points outside the data directory.
if st.Mode()&os.ModeSymlink == 0 {
ep, err := filepath.EvalSymlinks(resolved)
if err != nil {
if !os.IsNotExist(err) {
return err
}
} else if !fs.unsafeIsInDataDirectory(ep) {
return NewBadPathResolution(p, ep)
}
}
if st.IsDir() {
if s, err := fs.DirectorySize(resolved); err == nil {
fs.addDisk(-s)
}
} else {
fs.addDisk(-st.Size())
}
return os.RemoveAll(resolved) return os.RemoveAll(resolved)
} }

View File

@ -508,6 +508,80 @@ func TestFilesystem_Delete(t *testing.T) {
} }
}) })
g.It("deletes a symlink but not it's target within the root directory", func() {
// Symlink to a file inside the root directory.
err := os.Symlink(filepath.Join(rfs.root, "server/source.txt"), filepath.Join(rfs.root, "server/symlink.txt"))
g.Assert(err).IsNil()
// Delete the symlink itself.
err = fs.Delete("symlink.txt")
g.Assert(err).IsNil()
// Ensure the symlink was deleted.
_, err = os.Lstat(filepath.Join(rfs.root, "server/symlink.txt"))
g.Assert(err).IsNotNil()
// Ensure the symlink target still exists.
_, err = os.Lstat(filepath.Join(rfs.root, "server/source.txt"))
g.Assert(err).IsNil()
})
g.It("does not delete files symlinked outside of the root directory", func() {
// Create a file outside the root directory.
err := rfs.CreateServerFileFromString("/../source.txt", "test content")
g.Assert(err).IsNil()
// Create a symlink to the file outside the root directory.
err = os.Symlink(filepath.Join(rfs.root, "source.txt"), filepath.Join(rfs.root, "/server/symlink.txt"))
g.Assert(err).IsNil()
// Delete the symlink. (This should pass as we will delete the symlink itself, not it's target)
err = fs.Delete("symlink.txt")
g.Assert(err).IsNil()
// Ensure the file outside the root directory still exists.
_, err = os.Lstat(filepath.Join(rfs.root, "source.txt"))
g.Assert(err).IsNil()
})
g.It("does not delete files symlinked through a directory outside of the root directory", func() {
// Create a directory outside the root directory.
err := os.Mkdir(filepath.Join(rfs.root, "foo"), 0o755)
g.Assert(err).IsNil()
// Create a file inside the directory that is outside the root.
err = rfs.CreateServerFileFromString("/../foo/source.txt", "test content")
g.Assert(err).IsNil()
// Symlink the directory that is outside the root to a file inside the root.
err = os.Symlink(filepath.Join(rfs.root, "foo"), filepath.Join(rfs.root, "server/symlink"))
g.Assert(err).IsNil()
// Delete a file inside the symlinked directory.
err = fs.Delete("symlink/source.txt")
g.Assert(err).IsNotNil()
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
// Ensure the file outside the root directory still exists.
_, err = os.Lstat(filepath.Join(rfs.root, "foo/source.txt"))
g.Assert(err).IsNil()
})
g.It("returns an error when trying to delete a non-existent file symlinked through a directory outside of the root directory", func() {
// Create a directory outside the root directory.
err := os.Mkdir(filepath.Join(rfs.root, "foo2"), 0o755)
g.Assert(err).IsNil()
// Symlink the directory that is outside the root to a file inside the root.
err = os.Symlink(filepath.Join(rfs.root, "foo2"), filepath.Join(rfs.root, "server/symlink"))
g.Assert(err).IsNil()
// Delete a file inside the symlinked directory.
err = fs.Delete("symlink/source.txt")
g.Assert(err).IsNotNil()
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
})
g.AfterEach(func() { g.AfterEach(func() {
rfs.reset() rfs.reset()

View File

@ -2,6 +2,7 @@ package filesystem
import ( import (
"context" "context"
iofs "io/fs"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -33,8 +34,6 @@ func (fs *Filesystem) IsIgnored(paths ...string) error {
// This logic is actually copied over from the SFTP server code. Ideally that eventually // This logic is actually copied over from the SFTP server code. Ideally that eventually
// either gets ported into this application, or is able to make use of this package. // either gets ported into this application, or is able to make use of this package.
func (fs *Filesystem) SafePath(p string) (string, error) { func (fs *Filesystem) SafePath(p string) (string, error) {
var nonExistentPathResolution string
// Start with a cleaned up path before checking the more complex bits. // Start with a cleaned up path before checking the more complex bits.
r := fs.unsafeFilePath(p) r := fs.unsafeFilePath(p)
@ -44,47 +43,24 @@ func (fs *Filesystem) SafePath(p string) (string, error) {
if err != nil && !os.IsNotExist(err) { if err != nil && !os.IsNotExist(err) {
return "", errors.Wrap(err, "server/filesystem: failed to evaluate symlink") return "", errors.Wrap(err, "server/filesystem: failed to evaluate symlink")
} else if os.IsNotExist(err) { } else if os.IsNotExist(err) {
// The requested directory doesn't exist, so at this point we need to iterate up the // The target of one of the symlinks (EvalSymlinks is recursive) does not exist.
// path chain until we hit a directory that _does_ exist and can be validated. // So we get what target path does not exist and check if it's within the data
parts := strings.Split(filepath.Dir(r), "/") // directory. If it is, we return the original path, otherwise we return an error.
pErr, ok := err.(*iofs.PathError)
var try string if !ok {
// Range over all of the path parts and form directory pathings from the end return "", errors.Wrap(err, "server/filesystem: failed to evaluate symlink")
// moving up until we have a valid resolution or we run out of paths to try.
for k := range parts {
try = strings.Join(parts[:(len(parts)-k)], "/")
if !fs.unsafeIsInDataDirectory(try) {
break
} }
ep = pErr.Path
t, err := filepath.EvalSymlinks(try)
if err == nil {
nonExistentPathResolution = t
break
}
}
}
// If the new path doesn't start with their root directory there is clearly an escape
// attempt going on, and we should NOT resolve this path for them.
if nonExistentPathResolution != "" {
if !fs.unsafeIsInDataDirectory(nonExistentPathResolution) {
return "", NewBadPathResolution(p, nonExistentPathResolution)
}
// If the nonExistentPathResolution variable is not empty then the initial path requested
// did not exist and we looped through the pathway until we found a match. At this point
// we've confirmed the first matched pathway exists in the root server directory, so we
// can go ahead and just return the path that was requested initially.
return r, nil
} }
// If the requested directory from EvalSymlinks begins with the server root directory go // If the requested directory from EvalSymlinks begins with the server root directory go
// ahead and return it. If not we'll return an error which will block any further action // ahead and return it. If not we'll return an error which will block any further action
// on the file. // on the file.
if fs.unsafeIsInDataDirectory(ep) { if fs.unsafeIsInDataDirectory(ep) {
return ep, nil // Returning the original path here instead of the resolved path ensures that
// whatever the user is trying to do will work as expected. If we returned the
// resolved path, the user would be unable to know that it is in fact a symlink.
return r, nil
} }
return "", NewBadPathResolution(p, r) return "", NewBadPathResolution(p, r)

View File

@ -115,6 +115,14 @@ func TestFilesystem_Blocks_Symlinks(t *testing.T) {
panic(err) panic(err)
} }
if err := os.Symlink(filepath.Join(rfs.root, "malicious_does_not_exist.txt"), filepath.Join(rfs.root, "/server/symlinked_does_not_exist.txt")); err != nil {
panic(err)
}
if err := os.Symlink(filepath.Join(rfs.root, "/server/symlinked_does_not_exist.txt"), filepath.Join(rfs.root, "/server/symlinked_does_not_exist2.txt")); err != nil {
panic(err)
}
if err := os.Symlink(filepath.Join(rfs.root, "/malicious_dir"), filepath.Join(rfs.root, "/server/external_dir")); err != nil { if err := os.Symlink(filepath.Join(rfs.root, "/malicious_dir"), filepath.Join(rfs.root, "/server/external_dir")); err != nil {
panic(err) panic(err)
} }
@ -128,6 +136,22 @@ func TestFilesystem_Blocks_Symlinks(t *testing.T) {
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue() g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
}) })
g.It("cannot write to a non-existent file symlinked outside the root", func() {
r := bytes.NewReader([]byte("testing what the fuck"))
err := fs.Writefile("symlinked_does_not_exist.txt", r)
g.Assert(err).IsNotNil()
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
})
g.It("cannot write to chained symlinks with target that does not exist outside the root", func() {
r := bytes.NewReader([]byte("testing what the fuck"))
err := fs.Writefile("symlinked_does_not_exist2.txt", r)
g.Assert(err).IsNotNil()
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
})
g.It("cannot write a file to a directory symlinked outside the root", func() { g.It("cannot write a file to a directory symlinked outside the root", func() {
r := bytes.NewReader([]byte("testing")) r := bytes.NewReader([]byte("testing"))

View File

@ -447,7 +447,6 @@ func (ip *InstallationProcess) Execute() (string, error) {
"compress": "false", "compress": "false",
}, },
}, },
Privileged: true,
NetworkMode: container.NetworkMode(config.Get().Docker.Network.Mode), NetworkMode: container.NetworkMode(config.Get().Docker.Network.Mode),
UsernsMode: container.UsernsMode(config.Get().Docker.UsernsMode), UsernsMode: container.UsernsMode(config.Get().Docker.UsernsMode),
} }

View File

@ -1,3 +1,3 @@
package system package system
var Version = "develop" var Version = "1.7.5"