github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/helper/escapingfs/escapes.go (about)

     1  package escapingfs
     2  
     3  import (
     4  	"errors"
     5  	"os"
     6  	"path/filepath"
     7  	"strings"
     8  )
     9  
    10  // PathEscapesAllocViaRelative returns if the given path escapes the allocation
    11  // directory using relative paths.
    12  //
    13  // Only for use in server-side validation, where the real filesystem is not available.
    14  // For client-side validation use PathEscapesAllocDir, which includes symlink validation
    15  // as well.
    16  //
    17  // The prefix is joined to the path (e.g. "task/local"), and this function
    18  // checks if path escapes the alloc dir, NOT the prefix directory within the alloc dir.
    19  // With prefix="task/local", it will return false for "../secret", but
    20  // true for "../../../../../../root" path; only the latter escapes the alloc dir.
    21  func PathEscapesAllocViaRelative(prefix, path string) (bool, error) {
    22  	// Verify the destination does not escape the task's directory. The "alloc-dir"
    23  	// and "alloc-id" here are just placeholders; on a real filesystem they will
    24  	// have different names. The names are not important, but rather the number of levels
    25  	// in the path they represent.
    26  	alloc, err := filepath.Abs(filepath.Join("/", "alloc-dir/", "alloc-id/"))
    27  	if err != nil {
    28  		return false, err
    29  	}
    30  	abs, err := filepath.Abs(filepath.Join(alloc, prefix, path))
    31  	if err != nil {
    32  		return false, err
    33  	}
    34  	rel, err := filepath.Rel(alloc, abs)
    35  	if err != nil {
    36  		return false, err
    37  	}
    38  
    39  	return strings.HasPrefix(rel, ".."), nil
    40  }
    41  
    42  // pathEscapesBaseViaSymlink returns if path escapes dir, taking into account evaluation
    43  // of symlinks.
    44  //
    45  // The base directory must be an absolute path.
    46  func pathEscapesBaseViaSymlink(base, full string) (bool, error) {
    47  	resolveSym, err := filepath.EvalSymlinks(full)
    48  	if err != nil {
    49  		return false, err
    50  	}
    51  
    52  	rel, err := filepath.Rel(resolveSym, base)
    53  	if err != nil {
    54  		return true, nil
    55  	}
    56  
    57  	// note: this is not the same as !filesystem.IsAbs; we are asking if the relative
    58  	// path is descendent of the base path, indicating it does not escape.
    59  	isRelative := strings.HasPrefix(rel, "..") || rel == "."
    60  	escapes := !isRelative
    61  	return escapes, nil
    62  }
    63  
    64  // PathEscapesAllocDir returns true if base/prefix/path escapes the given base directory.
    65  //
    66  // Escaping a directory can be done with relative paths (e.g. ../../ etc.) or by
    67  // using symlinks. This checks both methods.
    68  //
    69  // The base directory must be an absolute path.
    70  func PathEscapesAllocDir(base, prefix, path string) (bool, error) {
    71  	full := filepath.Join(base, prefix, path)
    72  
    73  	// If base is not an absolute path, the caller passed in the wrong thing.
    74  	if !filepath.IsAbs(base) {
    75  		return false, errors.New("alloc dir must be absolute")
    76  	}
    77  
    78  	// Check path does not escape the alloc dir using relative paths.
    79  	if escapes, err := PathEscapesAllocViaRelative(prefix, path); err != nil {
    80  		return false, err
    81  	} else if escapes {
    82  		return true, nil
    83  	}
    84  
    85  	// Check path does not escape the alloc dir using symlinks.
    86  	if escapes, err := pathEscapesBaseViaSymlink(base, full); err != nil {
    87  		if os.IsNotExist(err) {
    88  			// Treat non-existent files as non-errors; perhaps not ideal but we
    89  			// have existing features (log-follow) that depend on this. Still safe,
    90  			// because we do the symlink check on every ReadAt call also.
    91  			return false, nil
    92  		}
    93  		return false, err
    94  	} else if escapes {
    95  		return true, nil
    96  	}
    97  
    98  	return false, nil
    99  }
   100  
   101  // PathEscapesSandbox returns whether previously cleaned path inside the
   102  // sandbox directory (typically this will be the allocation directory)
   103  // escapes.
   104  func PathEscapesSandbox(sandboxDir, path string) bool {
   105  	rel, err := filepath.Rel(sandboxDir, path)
   106  	if err != nil {
   107  		return true
   108  	}
   109  	if strings.HasPrefix(rel, "..") {
   110  		return true
   111  	}
   112  	return false
   113  }
   114  
   115  // EnsurePath is used to make sure a path exists
   116  func EnsurePath(path string, dir bool) error {
   117  	if !dir {
   118  		path = filepath.Dir(path)
   119  	}
   120  	return os.MkdirAll(path, 0755)
   121  }