github.com/argoproj/argo-cd/v3@v3.2.1/util/io/path/resolved.go (about)

     1  package path
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"net/url"
     7  	"os"
     8  	"path/filepath"
     9  	"strings"
    10  
    11  	log "github.com/sirupsen/logrus"
    12  )
    13  
    14  // ResolvedFilePath represents a resolved file path and is intended to prevent unintentional use of an unverified file
    15  // path. It is always either a URL or an absolute path.
    16  type ResolvedFilePath string
    17  
    18  // ResolvedFileOrDirectoryPath represents a resolved file or directory path and is intended to prevent unintentional use
    19  // of an unverified file or directory path. It is an absolute path.
    20  type ResolvedFileOrDirectoryPath string
    21  
    22  // resolveSymbolicLinkRecursive resolves the symlink path recursively to its
    23  // canonical path on the file system, with a maximum nesting level of maxDepth.
    24  // If path is not a symlink, returns the verbatim copy of path and err of nil.
    25  func resolveSymbolicLinkRecursive(path string, maxDepth int) (string, error) {
    26  	resolved, err := os.Readlink(path)
    27  	if err != nil {
    28  		// path is not a symbolic link
    29  		var pathErr *os.PathError
    30  		if errors.As(err, &pathErr) {
    31  			return path, nil
    32  		}
    33  		// Other error has occurred
    34  		return "", err
    35  	}
    36  
    37  	if maxDepth == 0 {
    38  		return "", errors.New("maximum nesting level reached")
    39  	}
    40  
    41  	// If we resolved to a relative symlink, make sure we use the absolute
    42  	// path for further resolving
    43  	if !strings.HasPrefix(resolved, string(os.PathSeparator)) {
    44  		basePath := filepath.Dir(path)
    45  		resolved = filepath.Join(basePath, resolved)
    46  	}
    47  
    48  	return resolveSymbolicLinkRecursive(resolved, maxDepth-1)
    49  }
    50  
    51  // isURLSchemeAllowed returns true if the protocol scheme is in the list of
    52  // allowed URL schemes.
    53  func isURLSchemeAllowed(scheme string, allowed []string) bool {
    54  	isAllowed := false
    55  	if len(allowed) > 0 {
    56  		for _, s := range allowed {
    57  			if strings.EqualFold(scheme, s) {
    58  				isAllowed = true
    59  				break
    60  			}
    61  		}
    62  	}
    63  
    64  	// Empty scheme means local file
    65  	return isAllowed && scheme != ""
    66  }
    67  
    68  // We do not provide the path in the error message, because it will be
    69  // returned to the user and could be used for information gathering.
    70  // Instead, we log the concrete error details.
    71  func resolveFailure(path string, err error) error {
    72  	log.Errorf("failed to resolve path '%s': %v", path, err)
    73  	return errors.New("internal error: failed to resolve path. Check logs for more details")
    74  }
    75  
    76  func ResolveFileOrDirectoryPath(appPath, repoRoot, dir string) (ResolvedFileOrDirectoryPath, error) {
    77  	path, err := resolveFileOrDirectory(appPath, repoRoot, dir, true)
    78  	if err != nil {
    79  		return "", err
    80  	}
    81  
    82  	return ResolvedFileOrDirectoryPath(path), nil
    83  }
    84  
    85  // ResolveValueFilePathOrUrl will inspect and resolve given file, and make sure that its final path is within the boundaries of
    86  // the path specified in repoRoot.
    87  //
    88  // appPath is the path we're operating in, e.g. where a Helm chart was unpacked
    89  // to. repoRoot is the path to the root of the repository.
    90  //
    91  // If either appPath or repoRoot is relative, it will be treated as relative
    92  // to the current working directory.
    93  //
    94  // valueFile is the path to a value file, relative to appPath. If valueFile is
    95  // specified as an absolute path (i.e. leading slash), it will be treated as
    96  // relative to the repoRoot. In case valueFile is a symlink in the extracted
    97  // chart, it will be resolved recursively and the decision of whether it is in
    98  // the boundary of repoRoot will be made using the final resolved path.
    99  // valueFile can also be a remote URL with a protocol scheme as prefix,
   100  // in which case the scheme must be included in the list of allowed schemes
   101  // specified by allowedURLSchemes.
   102  //
   103  // Will return an error if either valueFile is outside the boundaries of the
   104  // repoRoot, valueFile is an URL with a forbidden protocol scheme or if
   105  // valueFile is a recursive symlink nested too deep. May return errors for
   106  // other reasons as well.
   107  //
   108  // resolvedPath will hold the absolute, resolved path for valueFile on success
   109  // or set to the empty string on failure.
   110  //
   111  // isRemote will be set to true if valueFile is an URL using an allowed
   112  // protocol scheme, or to false if it resolved to a local file.
   113  func ResolveValueFilePathOrUrl(appPath, repoRoot, valueFile string, allowedURLSchemes []string) (resolvedPath ResolvedFilePath, isRemote bool, err error) { //nolint:revive //FIXME(var-naming)
   114  	// A value file can be specified as an URL to a remote resource.
   115  	// We only allow certain URL schemes for remote value files.
   116  	url, err := url.Parse(valueFile)
   117  	if err == nil {
   118  		// If scheme is empty, it means we parsed a path only
   119  		if url.Scheme != "" {
   120  			if isURLSchemeAllowed(url.Scheme, allowedURLSchemes) {
   121  				return ResolvedFilePath(valueFile), true, nil
   122  			}
   123  			return "", false, fmt.Errorf("the URL scheme '%s' is not allowed", url.Scheme)
   124  		}
   125  	}
   126  
   127  	path, err := resolveFileOrDirectory(appPath, repoRoot, valueFile, false)
   128  	if err != nil {
   129  		return "", false, err
   130  	}
   131  
   132  	return ResolvedFilePath(path), false, nil
   133  }
   134  
   135  func resolveFileOrDirectory(appPath string, repoRoot string, fileOrDirectory string, allowResolveToRoot bool) (string, error) {
   136  	// Ensure that our repository root is absolute
   137  	absRepoPath, err := filepath.Abs(repoRoot)
   138  	if err != nil {
   139  		return "", resolveFailure(repoRoot, err)
   140  	}
   141  
   142  	// If the path to the file or directory is relative, join it with the current working directory (appPath)
   143  	// Otherwise, join it with the repository's root
   144  	path := fileOrDirectory
   145  	if !filepath.IsAbs(path) {
   146  		absWorkDir, err := filepath.Abs(appPath)
   147  		if err != nil {
   148  			return "", resolveFailure(repoRoot, err)
   149  		}
   150  		path = filepath.Join(absWorkDir, path)
   151  	} else {
   152  		path = filepath.Join(absRepoPath, path)
   153  	}
   154  
   155  	// Ensure any symbolic link is resolved before we evaluate the path
   156  	delinkedPath, err := resolveSymbolicLinkRecursive(path, 10)
   157  	if err != nil {
   158  		return "", resolveFailure(repoRoot, err)
   159  	}
   160  	path = delinkedPath
   161  
   162  	// Resolve the joined path to an absolute path
   163  	path, err = filepath.Abs(path)
   164  	if err != nil {
   165  		return "", resolveFailure(repoRoot, err)
   166  	}
   167  
   168  	// Ensure our root path has a trailing slash, otherwise the following check
   169  	// would return true if root is /foo and path would be /foo2
   170  	requiredRootPath := absRepoPath
   171  	if !strings.HasSuffix(requiredRootPath, string(os.PathSeparator)) {
   172  		requiredRootPath += string(os.PathSeparator)
   173  	}
   174  
   175  	resolvedToRoot := path+string(os.PathSeparator) == requiredRootPath
   176  	if resolvedToRoot {
   177  		if !allowResolveToRoot {
   178  			return "", resolveFailure(path, errors.New("path resolved to repository root, which is not allowed"))
   179  		}
   180  	} else {
   181  		// Make sure that the resolved path to file is within the repository's root path
   182  		if !strings.HasPrefix(path, requiredRootPath) {
   183  			return "", fmt.Errorf("file '%s' resolved to outside repository root", fileOrDirectory)
   184  		}
   185  	}
   186  
   187  	return path, nil
   188  }