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
|
||
|
}
|