437bb4b86d
This PR adds symlink resolution when doing validation of paths to ensure they do not escape client allocation directories.
100 lines
3.2 KiB
Go
100 lines
3.2 KiB
Go
package escapingfs
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// PathEscapesAllocViaRelative returns if the given path escapes the allocation
|
|
// directory using relative paths.
|
|
//
|
|
// Only for use in server-side validation, where the real filesystem is not available.
|
|
// For client-side validation use PathEscapesAllocDir, which includes symlink validation
|
|
// as well.
|
|
//
|
|
// The prefix is joined to the path (e.g. "task/local"), and this function
|
|
// checks if path escapes the alloc dir, NOT the prefix directory within the alloc dir.
|
|
// With prefix="task/local", it will return false for "../secret", but
|
|
// true for "../../../../../../root" path; only the latter escapes the alloc dir.
|
|
func PathEscapesAllocViaRelative(prefix, path string) (bool, error) {
|
|
// Verify the destination does not escape the task's directory. The "alloc-dir"
|
|
// and "alloc-id" here are just placeholders; on a real filesystem they will
|
|
// have different names. The names are not important, but rather the number of levels
|
|
// in the path they represent.
|
|
alloc, err := filepath.Abs(filepath.Join("/", "alloc-dir/", "alloc-id/"))
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
abs, err := filepath.Abs(filepath.Join(alloc, prefix, path))
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
rel, err := filepath.Rel(alloc, abs)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return strings.HasPrefix(rel, ".."), nil
|
|
}
|
|
|
|
// pathEscapesBaseViaSymlink returns if path escapes dir, taking into account evaluation
|
|
// of symlinks.
|
|
//
|
|
// The base directory must be an absolute path.
|
|
func pathEscapesBaseViaSymlink(base, full string) (bool, error) {
|
|
resolveSym, err := filepath.EvalSymlinks(full)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
rel, err := filepath.Rel(resolveSym, base)
|
|
if err != nil {
|
|
return true, nil
|
|
}
|
|
|
|
// note: this is not the same as !filesystem.IsAbs; we are asking if the relative
|
|
// path is descendent of the base path, indicating it does not escape.
|
|
isRelative := strings.HasPrefix(rel, "..") || rel == "."
|
|
escapes := !isRelative
|
|
return escapes, nil
|
|
}
|
|
|
|
// PathEscapesAllocDir returns true if base/prefix/path escapes the given base directory.
|
|
//
|
|
// Escaping a directory can be done with relative paths (e.g. ../../ etc.) or by
|
|
// using symlinks. This checks both methods.
|
|
//
|
|
// The base directory must be an absolute path.
|
|
func PathEscapesAllocDir(base, prefix, path string) (bool, error) {
|
|
full := filepath.Join(base, prefix, path)
|
|
|
|
// If base is not an absolute path, the caller passed in the wrong thing.
|
|
if !filepath.IsAbs(base) {
|
|
return false, errors.New("alloc dir must be absolute")
|
|
}
|
|
|
|
// Check path does not escape the alloc dir using relative paths.
|
|
if escapes, err := PathEscapesAllocViaRelative(prefix, path); err != nil {
|
|
return false, err
|
|
} else if escapes {
|
|
return true, nil
|
|
}
|
|
|
|
// Check path does not escape the alloc dir using symlinks.
|
|
if escapes, err := pathEscapesBaseViaSymlink(base, full); err != nil {
|
|
if os.IsNotExist(err) {
|
|
// Treat non-existent files as non-errors; perhaps not ideal but we
|
|
// have existing features (log-follow) that depend on this. Still safe,
|
|
// because we do the symlink check on every ReadAt call also.
|
|
return false, nil
|
|
}
|
|
return false, err
|
|
} else if escapes {
|
|
return true, nil
|
|
}
|
|
|
|
return false, nil
|
|
}
|