163 lines
6.2 KiB
Go
163 lines
6.2 KiB
Go
|
package filesystem
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"golang.org/x/sync/errgroup"
|
||
|
"os"
|
||
|
"path/filepath"
|
||
|
"strings"
|
||
|
"sync"
|
||
|
)
|
||
|
|
||
|
// 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) {
|
||
|
var nonExistentPathResolution string
|
||
|
|
||
|
// 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.
|
||
|
p, err := filepath.EvalSymlinks(r)
|
||
|
if err != nil && !os.IsNotExist(err) {
|
||
|
return "", err
|
||
|
} else if os.IsNotExist(err) {
|
||
|
// The requested directory doesn't exist, so at this point we need to iterate up the
|
||
|
// path chain until we hit a directory that _does_ exist and can be validated.
|
||
|
parts := strings.Split(filepath.Dir(r), "/")
|
||
|
|
||
|
var try string
|
||
|
// Range over all of the path parts and form directory pathings 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
|
||
|
}
|
||
|
|
||
|
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 "", ErrBadPathResolution
|
||
|
}
|
||
|
|
||
|
// 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
|
||
|
// ahead and return it. If not we'll return an error which will block any further action
|
||
|
// on the file.
|
||
|
if fs.unsafeIsInDataDirectory(p) {
|
||
|
return p, nil
|
||
|
}
|
||
|
|
||
|
return "", ErrBadPathResolution
|
||
|
}
|
||
|
|
||
|
// 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(), "/")+"/")
|
||
|
}
|
||
|
|
||
|
// Helper function to keep some of the codebase a little cleaner. Returns a "safe" version of the path
|
||
|
// joined with a file. This is important because you cannot just assume that appending a file to a cleaned
|
||
|
// path will result in a cleaned path to that file. For example, imagine you have the following scenario:
|
||
|
//
|
||
|
// my_bad_file -> symlink:/etc/passwd
|
||
|
//
|
||
|
// cleaned := SafePath("../../etc") -> "/"
|
||
|
// filepath.Join(cleaned, my_bad_file) -> "/my_bad_file"
|
||
|
//
|
||
|
// You might think that "/my_bad_file" is fine since it isn't pointing to the original "../../etc/my_bad_file".
|
||
|
// However, this doesn't account for symlinks where the file might be pointing outside of the directory, so
|
||
|
// calling a function such as Chown against it would chown the symlinked location, and not the file within the
|
||
|
// Wings daemon.
|
||
|
func (fs *Filesystem) SafeJoin(dir string, f os.FileInfo) (string, error) {
|
||
|
if f.Mode()&os.ModeSymlink != 0 {
|
||
|
return fs.SafePath(filepath.Join(dir, f.Name()))
|
||
|
}
|
||
|
|
||
|
return filepath.Join(dir, f.Name()), nil
|
||
|
}
|
||
|
|
||
|
// 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.
|
||
|
var m = new(sync.Mutex)
|
||
|
var push = func(c string) {
|
||
|
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.
|
||
|
return cleaned, g.Wait()
|
||
|
}
|