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"));

Reply via email to