This is an automated email from the ASF dual-hosted git repository.
garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-bcel.git
The following commit(s) were added to refs/heads/master by this push:
new 135b3928 ClassPath.getBytes() sizes its buffer from the forged ZIP
uncompressed-size field (f016).
135b3928 is described below
commit 135b39283751a542e391ce40499bb874d6ff484d
Author: Gary Gregory <[email protected]>
AuthorDate: Fri Sep 4 17:31:04 2026 -0400
ClassPath.getBytes() sizes its buffer from the forged ZIP
uncompressed-size field (f016).
---
src/changes/changes.xml | 1 +
src/main/java/org/apache/bcel/util/ClassPath.java | 14 ++----
.../java/org/apache/bcel/util/ClassPathTest.java | 56 ++++++++++++++++++++++
3 files changed, 61 insertions(+), 10 deletions(-)
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 554a89e0..515a661a 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -103,6 +103,7 @@ The <action> type attribute can be add,update,fix,remove.
<action type="fix" dev="ggregory" due-to="Gary
Gregory">Silent u2/u1 count truncation across dump paths corrupts emitted
bytecode. (f013).</action>
<action type="fix" dev="ggregory" due-to="Gary
Gregory">Class2HTML emitters write attacker class-file strings into HTML
unescaped (stored XSS in reports) (f014).</action>
<action type="fix" dev="ggregory" due-to="Gary
Gregory">Class2HTML builds output file paths from the unvalidated class name
(f015).</action>
+ <action type="fix" dev="ggregory" due-to="Gary
Gregory">ClassPath.getBytes() sizes its buffer from the forged ZIP
uncompressed-size field (f016).</action>
<!-- ADD -->
<action type="add" dev="ggregory" due-to="nbauma109,
Gary Gregory">Add support for permitted subclasses #493.</action>
<action type="add" dev="ggregory" due-to="nbauma109,
Gary Gregory">Add RecordComponentInfo.getAttribute(byte tag)#494.</action>
diff --git a/src/main/java/org/apache/bcel/util/ClassPath.java
b/src/main/java/org/apache/bcel/util/ClassPath.java
index a08454be..ee73b479 100644
--- a/src/main/java/org/apache/bcel/util/ClassPath.java
+++ b/src/main/java/org/apache/bcel/util/ClassPath.java
@@ -19,7 +19,6 @@
package org.apache.bcel.util;
import java.io.Closeable;
-import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FilenameFilter;
@@ -631,19 +630,14 @@ public class ClassPath implements Closeable {
* @throws IOException Thrown if an I/O error occurs.
*/
public byte[] getBytes(final String name, final String suffix) throws
IOException {
- DataInputStream dis = null;
try (InputStream inputStream = getInputStream(name, suffix)) {
if (inputStream == null) {
throw new IOException("Couldn't find: " + name + suffix);
}
- dis = new DataInputStream(inputStream);
- final byte[] bytes = new byte[inputStream.available()];
- dis.readFully(bytes);
- return bytes;
- } finally {
- if (dis != null) {
- dis.close();
- }
+ // Read until EOF instead of sizing the buffer from
InputStream.available(): for ZIP/JAR entries, available()
+ // reflects the archive's declared uncompressed-size field, which
is untrusted metadata. Trusting it lets a
+ // tiny archive force a forged multi-gigabyte allocation, or
silently truncate the returned bytes.
+ return IOUtils.toByteArray(inputStream);
}
}
diff --git a/src/test/java/org/apache/bcel/util/ClassPathTest.java
b/src/test/java/org/apache/bcel/util/ClassPathTest.java
index dfcf497a..1e8d1198 100644
--- a/src/test/java/org/apache/bcel/util/ClassPathTest.java
+++ b/src/test/java/org/apache/bcel/util/ClassPathTest.java
@@ -18,17 +18,43 @@
*/
package org.apache.bcel.util;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
import org.apache.bcel.AbstractTest;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
class ClassPathTest extends AbstractTest {
+ private static int lastIndexOf(final byte[] haystack, final byte[] needle)
{
+ outer: for (int i = haystack.length - needle.length; i >= 0; i--) {
+ for (int j = 0; j < needle.length; j++) {
+ if (haystack[i + j] != needle[j]) {
+ continue outer;
+ }
+ }
+ return i;
+ }
+ return -1;
+ }
+
+ private static void writeLittleEndianInt(final byte[] bytes, final int
offset, final int value) {
+ bytes[offset] = (byte) value;
+ bytes[offset + 1] = (byte) (value >>> 8);
+ bytes[offset + 2] = (byte) (value >>> 16);
+ bytes[offset + 3] = (byte) (value >>> 24);
+ }
+
@Test
void testClose() throws IOException {
try (ClassPath cp = new ClassPath(ClassPath.getClassPath())) {
@@ -36,6 +62,36 @@ class ClassPathTest extends AbstractTest {
}
}
+ /**
+ * The buffer for {@code getBytes} must be sized from the actual entry
content, not from the archive's declared
+ * uncompressed-size field, which is attacker-controlled and may be forged
(huge: forged allocation; small: silent
+ * truncation).
+ */
+ @Test
+ void testGetBytesIgnoresForgedUncompressedSize(@TempDir final Path
tempDir) throws IOException {
+ final byte[] content = new byte[256];
+ for (int i = 0; i < content.length; i++) {
+ content[i] = (byte) i;
+ }
+ final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try (ZipOutputStream zos = new ZipOutputStream(baos)) {
+ zos.putNextEntry(new ZipEntry("Foo.class"));
+ zos.write(content);
+ zos.closeEntry();
+ }
+ final byte[] jar = baos.toByteArray();
+ // Forge the uncompressed-size field (offset 24) of the central
directory file header (PK\1\2),
+ // which is where java.util.zip.ZipFile reads entry sizes from.
+ final int cen = lastIndexOf(jar, new byte[] { 0x50, 0x4B, 0x01, 0x02
});
+ assertTrue(cen >= 0);
+ writeLittleEndianInt(jar, cen + 24, 64 * 1024 * 1024);
+ final Path jarFile = tempDir.resolve("forged.jar");
+ Files.write(jarFile, jar);
+ try (ClassPath classPath = new ClassPath(jarFile.toString())) {
+ assertArrayEquals(content, classPath.getBytes("Foo"));
+ }
+ }
+
@Test
void testGetClassFile() throws IOException {
assertNotNull(ClassPath.SYSTEM_CLASS_PATH.getClassFile("java.lang.String"));