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 }