VYPR
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.

PackageAffected versionsPatched versions
org.pf4j:pf4jMaven
< 3.14.13.14.1

Affected products

1

Patches

1
20c2f80089d1

Fix path traversal vulnerabilities in ZIP extraction (issues #618, #623)

https://github.com/pf4j/pf4jDecebal SuiuJan 6, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.