2020-09-27 19:24:08 +00:00
|
|
|
package filesystem
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2023-02-08 02:14:35 +00:00
|
|
|
iofs "io/fs"
|
2020-09-27 19:24:08 +00:00
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
|
|
|
"sync"
|
2021-01-11 00:33:39 +00:00
|
|
|
|
2021-01-31 02:43:35 +00:00
|
|
|
"emperror.dev/errors"
|
2021-01-11 00:33:39 +00:00
|
|
|
"golang.org/x/sync/errgroup"
|
2020-09-27 19:24:08 +00:00
|
|
|
)
|
|
|
|
|
2021-01-11 00:33:39 +00:00
|
|
|
// 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) {
|
2021-04-17 20:29:18 +00:00
|
|
|
return errors.WithStack(&Error{code: ErrCodeDenylistFile, path: p, resolved: sp})
|
2021-01-11 00:33:39 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-09-27 19:24:08 +00:00
|
|
|
// 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
|
|
|
|
// path it is returned. If they managed to "escape" an error will be returned.
|
|
|
|
//
|
|
|
|
// 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.
|
|
|
|
func (fs *Filesystem) SafePath(p string) (string, error) {
|
|
|
|
// Start with a cleaned up path before checking the more complex bits.
|
|
|
|
r := fs.unsafeFilePath(p)
|
|
|
|
|
|
|
|
// At the same time, evaluate the symlink status and determine where this file or folder
|
|
|
|
// is truly pointing to.
|
2020-11-08 21:52:20 +00:00
|
|
|
ep, err := filepath.EvalSymlinks(r)
|
2020-09-27 19:24:08 +00:00
|
|
|
if err != nil && !os.IsNotExist(err) {
|
2021-01-31 02:43:35 +00:00
|
|
|
return "", errors.Wrap(err, "server/filesystem: failed to evaluate symlink")
|
2020-09-27 19:24:08 +00:00
|
|
|
} else if os.IsNotExist(err) {
|
2023-02-08 02:14:35 +00:00
|
|
|
// The target of one of the symlinks (EvalSymlinks is recursive) does not exist.
|
|
|
|
// So we get what target path does not exist and check if it's within the data
|
|
|
|
// directory. If it is, we return the original path, otherwise we return an error.
|
|
|
|
pErr, ok := err.(*iofs.PathError)
|
|
|
|
if !ok {
|
|
|
|
return "", errors.Wrap(err, "server/filesystem: failed to evaluate symlink")
|
2020-09-27 19:24:08 +00:00
|
|
|
}
|
2023-02-08 02:14:35 +00:00
|
|
|
ep = pErr.Path
|
2020-09-27 19:24:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
// on the file.
|
2020-11-08 21:52:20 +00:00
|
|
|
if fs.unsafeIsInDataDirectory(ep) {
|
2023-02-08 02:14:35 +00:00
|
|
|
// 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
|
2020-09-27 19:24:08 +00:00
|
|
|
}
|
|
|
|
|
2020-11-08 21:52:20 +00:00
|
|
|
return "", NewBadPathResolution(p, r)
|
2020-09-27 19:24:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Generate a path to the file by cleaning it up and appending the root server path to it. This
|
|
|
|
// DOES NOT guarantee that the file resolves within the server data directory. You'll want to use
|
|
|
|
// the fs.unsafeIsInDataDirectory(p) function to confirm.
|
|
|
|
func (fs *Filesystem) unsafeFilePath(p string) string {
|
|
|
|
// Calling filepath.Clean on the joined directory will resolve it to the absolute path,
|
|
|
|
// removing any ../ type of resolution arguments, and leaving us with a direct path link.
|
|
|
|
//
|
|
|
|
// This will also trim the existing root path off the beginning of the path passed to
|
|
|
|
// the function since that can get a bit messy.
|
|
|
|
return filepath.Clean(filepath.Join(fs.Path(), strings.TrimPrefix(p, fs.Path())))
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check that that path string starts with the server data directory path. This function DOES NOT
|
|
|
|
// validate that the rest of the path does not end up resolving out of this directory, or that the
|
|
|
|
// targeted file or folder is not a symlink doing the same thing.
|
|
|
|
func (fs *Filesystem) unsafeIsInDataDirectory(p string) bool {
|
|
|
|
return strings.HasPrefix(strings.TrimSuffix(p, "/")+"/", strings.TrimSuffix(fs.Path(), "/")+"/")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Executes the fs.SafePath function in parallel against an array of paths. If any of the calls
|
|
|
|
// fails an error will be returned.
|
|
|
|
func (fs *Filesystem) ParallelSafePath(paths []string) ([]string, error) {
|
|
|
|
var cleaned []string
|
|
|
|
|
|
|
|
// Simple locker function to avoid racy appends to the array of cleaned paths.
|
2021-11-15 17:37:56 +00:00
|
|
|
m := new(sync.Mutex)
|
|
|
|
push := func(c string) {
|
2020-09-27 19:24:08 +00:00
|
|
|
m.Lock()
|
|
|
|
cleaned = append(cleaned, c)
|
|
|
|
m.Unlock()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create an error group that we can use to run processes in parallel while retaining
|
|
|
|
// the ability to cancel the entire process immediately should any of it fail.
|
|
|
|
g, ctx := errgroup.WithContext(context.Background())
|
|
|
|
|
|
|
|
// Iterate over all of the paths and generate a cleaned path, if there is an error for any
|
|
|
|
// of the files, abort the process.
|
|
|
|
for _, p := range paths {
|
|
|
|
// Create copy so we can use it within the goroutine correctly.
|
|
|
|
pi := p
|
|
|
|
|
|
|
|
// Recursively call this function to continue digging through the directory tree within
|
|
|
|
// a separate goroutine. If the context is canceled abort this process.
|
|
|
|
g.Go(func() error {
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
return ctx.Err()
|
|
|
|
default:
|
|
|
|
// If the callback returns true, go ahead and keep walking deeper. This allows
|
|
|
|
// us to programmatically continue deeper into directories, or stop digging
|
|
|
|
// if that pathway knows it needs nothing else.
|
|
|
|
if c, err := fs.SafePath(pi); err != nil {
|
|
|
|
return err
|
|
|
|
} else {
|
|
|
|
push(c)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// Block until all of the routines finish and have returned a value.
|
2020-11-28 23:57:10 +00:00
|
|
|
return cleaned, g.Wait()
|
2020-09-27 19:24:08 +00:00
|
|
|
}
|