{"record":{"id":"33cbc3f0bae66e5b","repo":"spring-projects/spring-ai","slug":"resource-uri-resolves-outside-the-cache-directory","errorCode":null,"errorMessage":"Resource URI resolves outside the cache directory: ","messagePattern":"Resource URI resolves outside the cache directory: ","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"models/spring-ai-transformers/src/main/java/org/springframework/ai/transformers/ResourceCacheService.java","lineNumber":140,"sourceCode":"\t\t\t\tFileCopyUtils.copy(StreamUtils.copyToByteArray(originalResource.getInputStream()), cachedFile);\n\t\t\t\tlogger.info(\"Caching the \" + originalResource.toString() + \" resource to: \" + cachedFile);\n\t\t\t}\n\t\t\treturn new FileUrlResource(cachedFile.getAbsolutePath());\n\t\t}\n\t\tcatch (Exception e) {\n\t\t\tthrow new IllegalStateException(\"Failed to cache the resource: \" + originalResource.getDescription(), e);\n\t\t}\n\t}\n\n\tprivate File getCachedFile(Resource originalResource) throws IOException {\n\t\tvar resourceParentFolder = new File(this.cacheDirectory,\n\t\t\t\tUUID.nameUUIDFromBytes(pathWithoutLastSegment(originalResource.getURI())).toString());\n\t\tresourceParentFolder.mkdirs();\n\t\tString newFileName = getCacheName(originalResource);\n\t\tFile cachedFile = new File(resourceParentFolder, newFileName);\n\t\tString canonicalCache = this.cacheDirectory.getCanonicalPath() + File.separator;\n\t\tif (!cachedFile.getCanonicalPath().startsWith(canonicalCache)) {\n\t\t\tthrow new IllegalArgumentException(\n\t\t\t\t\t\"Resource URI resolves outside the cache directory: \" + originalResource.getDescription());\n\t\t}\n\t\treturn cachedFile;\n\t}\n\n\tprivate byte[] pathWithoutLastSegment(URI uri) {\n\t\tString path = uri.toASCIIString();\n\t\tvar pathBeforeLastSegment = path.substring(0, path.lastIndexOf('/') + 1);\n\t\treturn pathBeforeLastSegment.getBytes();\n\t}\n\n\tprivate String getCacheName(Resource originalResource) throws IOException {\n\t\tString fileName = originalResource.getFilename();\n\t\tAssert.hasText(fileName, \"The file name must should not be null or empty\");\n\t\tString fragment = originalResource.getURI().getFragment();\n\t\treturn !StringUtils.hasText(fragment) ? fileName : fileName + \"_\" + fragment;\n\t}\n","sourceCodeStart":122,"sourceCodeEnd":158,"githubUrl":"https://github.com/spring-projects/spring-ai/blob/98a7beda4f29d80a71c5837eb4053b03a93a46f7/models/spring-ai-transformers/src/main/java/org/springframework/ai/transformers/ResourceCacheService.java#L122-L158","documentation":"getCachedFile computes the target cache file from a UUID of the resource URI and then performs a canonical-path containment check: the resolved file must live inside the cache directory. If the canonical path escapes the cache root (symlink trickery or crafted resource names), it throws IllegalArgumentException to block a path-traversal style escape.","triggerScenarios":"Calling getCachedResource (via cachedFile) with a Resource whose URI path, after canonicalization, resolves outside the configured cache directory — e.g. URIs containing ../ segments or pointing through symlinks out of the cache root.","commonSituations":"Passing user-supplied URLs/paths as model resources; symlinks inside the cache directory pointing elsewhere; resources on different mount points that canonicalize outside the cache root.","solutions":["Use plain, well-formed model resource URIs that resolve within the cache directory; remove ../ segments and symlinks.","Point the cache directory at the real (canonical) location, not a symlinked path.","If resources legitimately live elsewhere, copy them into the cache directory first or configure the cache dir accordingly."],"exampleFix":"// before\nnew UrlResource(\"file:/models/../../etc/model.onnx\") // escapes cache dir\n// after\nnew UrlResource(\"https://huggingface.co/model/onnx/model.onnx\") // normal remote resource","handlingStrategy":"validation","validationCode":"File cacheDir = new File(\"/var/cache/spring-ai\");\nFile target = new File(resource.getURI().getPath());\nif (!target.getCanonicalPath().startsWith(cacheDir.getCanonicalPath() + File.separator)) {\n    throw new IllegalArgumentException(\"Refusing resource outside cache dir\");\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never pass user-controlled file paths as model resources.","Avoid symlinked cache directories; use canonical paths in config.","Normalize URIs (remove ../) before handing resources to the cache."],"tags":["security","path-traversal","cache","validation"],"backgroundTag":"path-traversal-blocked","analyzedSha":"98a7beda4f29d80a71c5837eb4053b03a93a46f7","analyzedAt":"2026-09-11T14:15:49.441Z","contentChangedAt":"2026-09-11T14:15:49.441Z","schemaVersion":2},"datasetVersion":"2026-09-14T11:17:12.474Z"}