Compare commits
6 Commits
develop
...
v1.7.5-hot
Author | SHA1 | Date | |
---|---|---|---|
|
a10a3ed475 | ||
|
c0aff69d2b | ||
|
fb65487ed8 | ||
|
dcbc59790d | ||
|
00451b38db | ||
|
9f6548eaa8 |
12
CHANGELOG.md
12
CHANGELOG.md
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"))
|
||||||
|
|
||||||
|
|
|
@ -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),
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user