diff --git a/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java b/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java index 97cc4bc9b33..f3234f2c96b 100644 --- a/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java +++ b/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java @@ -614,10 +614,12 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol private Set getClassPathManifestEntriesFromJar(File jar) throws IOException { URL base = jar.toURI().toURL(); File parent = jar.getAbsoluteFile().getParentFile(); + try (JarFile jarFile = new JarFile(jar)) { Manifest manifest = jarFile.getManifest(); Attributes attributes = (manifest != null ? manifest.getMainAttributes() : null); String classPath = (attributes != null ? attributes.getValue(Name.CLASS_PATH) : null); + Set manifestEntries = new LinkedHashSet<>(); if (StringUtils.hasLength(classPath)) { StringTokenizer tokenizer = new StringTokenizer(classPath); @@ -627,8 +629,15 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol // See jdk.internal.loader.URLClassPath.JarLoader.tryResolveFile(URL, String) continue; } - File candidate = new File(parent, path); - if (candidate.isFile() && candidate.getCanonicalPath().contains(parent.getCanonicalPath())) { + + // Handle absolute paths correctly: do not apply parent to absolute paths. + File pathFile = new File(path); + File candidate = (pathFile.isAbsolute() ? pathFile : new File(parent, path)); + + // For relative paths, enforce security check: must be under parent. + // For absolute paths, just verify file exists (matching JVM behavior). + if (candidate.isFile() && (pathFile.isAbsolute() || + candidate.getCanonicalPath().contains(parent.getCanonicalPath()))) { manifestEntries.add(ClassPathManifestEntry.of(candidate, this.useCaches)); } } diff --git a/spring-core/src/test/java/org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.java b/spring-core/src/test/java/org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.java index a5943124de4..e0390b6c7b0 100644 --- a/spring-core/src/test/java/org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.java @@ -337,6 +337,21 @@ class PathMatchingResourcePatternResolverTests { assertThat(result.replace("\\", "/")).contains("!!!!").contains("/lib/asset.jar!/assets/file.txt"); } + @Test + void javaDashJarFindsAbsoluteClassPathManifestEntries() throws Exception { + Path assetJar = this.temp.resolve("dependency").resolve("asset.jar"); + Files.createDirectories(assetJar.getParent()); + writeAssetJar(assetJar); + writeApplicationJarWithAbsolutePath(this.temp.resolve("app.jar"), assetJar); + String java = ProcessHandle.current().info().command().get(); + Process process = new ProcessBuilder(java, "-jar", "app.jar") + .directory(this.temp.toFile()) + .start(); + assertThat(process.waitFor()).isZero(); + String result = StreamUtils.copyToString(process.getInputStream(), StandardCharsets.UTF_8); + assertThat(result.replace("\\", "/")).contains("!!!!").contains("asset.jar!/assets/file.txt"); + } + private void writeAssetJar(Path path) throws Exception { try (JarOutputStream jar = new JarOutputStream(new FileOutputStream(path.toFile()))) { jar.putNextEntry(new ZipEntry("assets/")); @@ -392,6 +407,35 @@ class PathMatchingResourcePatternResolverTests { assertThat(new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + path + ResourceUtils.JAR_URL_SEPARATOR).exists()).isTrue(); } + private void writeApplicationJarWithAbsolutePath(Path path, Path assetJar) throws Exception { + Manifest manifest = new Manifest(); + Attributes mainAttributes = manifest.getMainAttributes(); + mainAttributes.put(Name.CLASS_PATH, buildSpringClassPath() + assetJar.toAbsolutePath()); + mainAttributes.put(Name.MAIN_CLASS, ClassPathManifestEntriesTestApplication.class.getName()); + mainAttributes.put(Name.MANIFEST_VERSION, "1.0"); + try (JarOutputStream jar = new JarOutputStream(new FileOutputStream(path.toFile()), manifest)) { + String appClassResource = ClassUtils.convertClassNameToResourcePath( + ClassPathManifestEntriesTestApplication.class.getName()) + ClassUtils.CLASS_FILE_SUFFIX; + String folder = ""; + for (String name : appClassResource.split("/")) { + if (!name.endsWith(ClassUtils.CLASS_FILE_SUFFIX)) { + folder += name + "/"; + jar.putNextEntry(new ZipEntry(folder)); + jar.closeEntry(); + } + else { + jar.putNextEntry(new ZipEntry(folder + name)); + try (InputStream in = getClass().getResourceAsStream(name)) { + in.transferTo(jar); + } + jar.closeEntry(); + } + } + } + assertThat(new FileSystemResource(path).exists()).isTrue(); + assertThat(new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + path + ResourceUtils.JAR_URL_SEPARATOR).exists()).isTrue(); + } + private String buildSpringClassPath() throws Exception { return copyClasses(PathMatchingResourcePatternResolver.class, "spring-core") + copyClasses(LogFactory.class, "commons-logging");