High severity7.5NVD Advisory· Published Mar 25, 2026· Updated Apr 1, 2026
CVE-2025-70952
CVE-2025-70952
Description
pf4j before 20c2f80 has a path traversal vulnerability in the extract() function of Unzip.java, where improper handling of zip entry names can allow directory traversal or Zip Slip attacks, due to a lack of proper path normalization and validation.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.pf4j:pf4jMaven | < 3.14.1 | 3.14.1 |
Affected products
1Patches
120c2f80089d1Fix path traversal vulnerabilities in ZIP extraction (issues #618, #623)
3 files changed · +105 −54
pf4j/src/main/java/module-info.java+0 −43 removed@@ -1,43 +0,0 @@ -/* - * Copyright (C) 2012-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Module descriptor for PF4J. - * - * @author Decebal Suiu - * @author Andreas Rudolph - */ -module org.pf4j { - requires java.base; - - // provides javax.annotation - requires java.compiler; - - // provided by the ASM library, use "requires static" since it's optional - requires static org.objectweb.asm; - - requires org.slf4j; - - // The java-semver library currently does not provide a module. - // Maybe we should send them a pull request, that at least they provide an - // automatic module name in their MANIFEST file. - requires com.github.zafarkhaja.semver; - - // Maybe we should reconsider the package hierarchy, that only classes are - // exported, which are required by 3rd party developers. - exports org.pf4j; - exports org.pf4j.processor; -}
pf4j/src/main/java/org/pf4j/util/Unzip.java+33 −5 modified@@ -22,6 +22,7 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.nio.file.Path; import java.util.zip.ZipEntry; import java.util.zip.ZipException; import java.util.zip.ZipInputStream; @@ -75,16 +76,13 @@ public void extract() throws IOException { FileUtils.delete(destination.toPath()); } - String destinationCanonicalPath = destination.getCanonicalPath(); try (ZipInputStream zipInputStream = new ZipInputStream(new FileInputStream(source))) { ZipEntry zipEntry; while ((zipEntry = zipInputStream.getNextEntry()) != null) { File file = new File(destination, zipEntry.getName()); - String fileCanonicalPath = file.getCanonicalPath(); - if (!fileCanonicalPath.startsWith(destinationCanonicalPath)) { - throw new ZipException("The file "+ zipEntry.getName() + " is trying to leave the target output directory of "+ destination); - } + // Validate path before any file operations + validateExtractPath(destination, file, zipEntry.getName()); // create intermediary directories - sometimes zip don't add them File dir = new File(file.getParent()); @@ -106,6 +104,36 @@ public void extract() throws IOException { } } + /** + * Validates that the extraction path is within the destination directory. + * Uses Path API with normalization to prevent directory traversal attacks. + * + * @param destination the intended extraction directory + * @param file the file to be extracted + * @param entryName the zip entry name (for error messages) + * @throws ZipException if the file would be extracted outside the destination + */ + private void validateExtractPath(File destination, File file, String entryName) throws ZipException { + try { + // Normalize and resolve to absolute paths to eliminate ".." and "." components + Path destinationPath = destination.toPath().toRealPath(); + Path filePath = file.toPath().normalize().toAbsolutePath(); + + // Path.startsWith() performs proper path component comparison, not string prefix matching + if (!filePath.startsWith(destinationPath)) { + throw new ZipException("Entry '" + entryName + "' is attempting to write outside the target directory: " + destination); + } + } catch (IOException e) { + // If toRealPath() fails (destination doesn't exist), use normalize + toAbsolutePath + Path destinationPath = destination.toPath().normalize().toAbsolutePath(); + Path filePath = file.toPath().normalize().toAbsolutePath(); + + if (!filePath.startsWith(destinationPath)) { + throw new ZipException("Entry '" + entryName + "' is attempting to write outside the target directory: " + destination); + } + } + } + private static void mkdirsOrThrow(File dir) throws IOException { if (!dir.exists() && !dir.mkdirs()) { throw new IOException("Failed to create directory " + dir);
pf4j/src/test/java/org/pf4j/util/UnzipTest.java+72 −6 modified@@ -41,19 +41,85 @@ public void zipSlip() throws IOException { unzip.setDestination(destination.toFile()); Exception exception = assertThrows(ZipException.class, unzip::extract); - assertTrue(exception.getMessage().contains("is trying to leave the target output directory")); + assertTrue(exception.getMessage().contains("attempting to write outside the target directory")); + } + + /** + * Test for issue #618: Bypass using sibling directory with matching prefix. + * Attack: If destination is "/tmp/zipSlip", attacker uses "../zipSlip_evil/malicious.sh" + * The canonical path becomes "/tmp/zipSlip_evil/malicious.sh" which startsWith("/tmp/zipSlip") + * This demonstrates the vulnerability: the file SHOULD be rejected but validation passes. + */ + @Test + public void zipSlipBypasWithSiblingDirectory() throws IOException { + Path tempDir = Files.createTempDirectory("test"); + Path destination = Files.createDirectory(tempDir.resolve("zipSlip")); + Path siblingDir = Files.createDirectory(tempDir.resolve("zipSlip_evil")); + + // Create malicious zip that tries to escape to sibling directory + File zipFile = createZipWithEntry("../zipSlip_evil/malicious.sh"); + + Unzip unzip = new Unzip(); + unzip.setSource(zipFile); + unzip.setDestination(destination.toFile()); + + Exception exception = assertThrows(ZipException.class, unzip::extract); + assertTrue(exception.getMessage().contains("attempting to write outside the target directory")); + } + + /** + * Test for issue #623: Bypass using partial prefix match. + * Attack: If destination is "/tmp/dir", path "/tmp/directory/file" passes startsWith check + * This demonstrates the vulnerability: the file SHOULD be rejected but validation passes. + */ + @Test + public void zipSlipBypassWithPartialPrefixMatch() throws IOException { + Path tempDir = Files.createTempDirectory("test"); + Path destination = Files.createDirectory(tempDir.resolve("dir")); + Path similarDir = Files.createDirectory(tempDir.resolve("directory")); + + // Create malicious zip that tries to escape to directory with similar name + File zipFile = createZipWithEntry("../directory/malicious.sh"); + + Unzip unzip = new Unzip(); + unzip.setSource(zipFile); + unzip.setDestination(destination.toFile()); + + Exception exception = assertThrows(ZipException.class, unzip::extract); + assertTrue(exception.getMessage().contains("attempting to write outside the target directory")); + } + + /** + * Positive test: Verify legitimate nested paths work correctly. + */ + @Test + public void extractLegitimateNestedPaths() throws IOException { + Path destination = Files.createTempDirectory("legitimate"); + File zipFile = createZipWithEntry("subdir/nested/file.txt"); + + Unzip unzip = new Unzip(); + unzip.setSource(zipFile); + unzip.setDestination(destination.toFile()); + + // Should extract without throwing exception + unzip.extract(); + + Path extractedFile = destination.resolve("subdir/nested/file.txt"); + assertTrue(Files.exists(extractedFile)); } private File createMaliciousZipFile() throws IOException { - File zipFile = File.createTempFile("malicious", ".zip"); - String maliciousFileName = "../malicious.sh"; + return createZipWithEntry("../malicious.sh"); + } + + private File createZipWithEntry(String entryName) throws IOException { + File zipFile = File.createTempFile("test", ".zip"); try (ZipOutputStream zipOutputStream = new ZipOutputStream(new FileOutputStream(zipFile))) { - ZipEntry entry = new ZipEntry(maliciousFileName); + ZipEntry entry = new ZipEntry(entryName); zipOutputStream.putNextEntry(entry); - zipOutputStream.write("Malicious content".getBytes()); + zipOutputStream.write("Test content".getBytes()); zipOutputStream.closeEntry(); } - return zipFile; }
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/pf4j/pf4j/commit/20c2f80089d1ea779e22c2de5f109a0bce4e1b14nvdPatchWEB
- github.com/pf4j/pf4j/issues/623nvdExploitIssue TrackingThird Party AdvisoryWEB
- gist.github.com/weaver4VD/410f23adb24ef5f5077f021f4393e705nvdThird Party AdvisoryWEB
- github.com/advisories/GHSA-5458-7hh9-v7p4ghsaADVISORY
- github.com/pf4j/pf4j/issues/618nvdIssue TrackingThird Party AdvisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2025-70952ghsaADVISORY
News mentions
0No linked articles in our index yet.