Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8354135

ZipInputStream throws on large LOC headers

XMLWordPrintable

    • b21
    • 24
    • generic
    • generic

      ADDITIONAL SYSTEM INFORMATION :
      MacOS/arm64, Linux/amd64, any JDK 24 distribution (tried temurin, zulu, adoptopenjdk)

      A DESCRIPTION OF THE PROBLEM :
      If a zip file contains local entries with large extras (65505 bytes) with 0-len name and comments then reading that file with `ZipInputStream` will fail.

      While such files aren't technically compliant to the spec (these entries can only have local file headers as they don't fit in a central file header) there are implementations out there that emit zips like this, notably ZipFlinger:

      > If an entry has been deleted in the middle of the archive, Zipflinger will not leave a “hole” there. This is done in order to be compatible with top-down parsers such as jarsigner or the JDK zip classes. To this effect, Zipflinger fills empty space with virtual entries (a.k.a a Local File Header with no name, up to 64KiB extra and no Central Directory entry). Alignment is also done via “extra field”.

      https://android.googlesource.com/platform/tools/base/+/refs/heads/mirror-goog-studio-master-dev/zipflinger/

      REGRESSION : Last worked in version 23.0.2

      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      Construct a zip file with a large LOC header.

      EXPECTED VERSUS ACTUAL BEHAVIOR :
      EXPECTED -
      ZipInputStream does not crash when iterating over the file.
      ACTUAL -
      ZipInputStream throws IllegalArgumentException:

      Exception in thread "main" java.lang.IllegalArgumentException: invalid extra field length
      at java.base/java.util.zip.ZipEntry.setExtra0(ZipEntry.java:550)
      at java.base/java.util.zip.ZipInputStream.readLOC(ZipInputStream.java:556)
      at java.base/java.util.zip.ZipInputStream.getNextEntry(ZipInputStream.java:158)
      at Zip.main(Zip.java:90)

      ---------- BEGIN SOURCE ----------
      import java.io.IOException;
      import java.nio.file.Files;
      import java.util.stream.Collectors;
      import java.util.stream.IntStream;
      import java.util.stream.Stream;
      import java.util.zip.ZipEntry;
      import java.util.zip.ZipFile;
      import java.util.zip.ZipInputStream;

      public class Zip {
        public static final String hex = """
            # file 1: file.txt

            504b 0304 # local file header signature (0x04034b50)
            0a00 # version needed to extract (10)
            0000 # general purpose bit flag
            0000 # compression method (STORE)
            0000 # last modified file time
            0000 # last modified file date
            dddd 147d # CRC-32
            0d00 0000 # compressed size, 13 bytes
            0d00 0000 # uncompressed size, 13 bytes
            0800 # file name length, 8 bytes
            0000 # extra field length, 0 bytes
          
            # filename
            6669 6c65 2e74 7874 # "file.txt"
          
            # actual file contents
            4865 6c6c 6f20 576f 726c 6421 0a # "Hello world!\\n"

            # file 2: a hole - dummy empty file with a large extra

            504b 0304 # local file header signature (0x04034b50)
            0a00 # version needed to extract (10)
            0000 # general purpose bit flag
            0000 # compression method (STORE)
            0000 # last modified file time
            0000 # last modified file date
            0000 0000 # CRC-32
            0000 0000 # compressed size, 0 bytes
            0000 0000 # uncompressed size, 0 bytes
            0000 # file name length, 0 bytes
            e1ff # extra field length, 65505 bytes

            # extra field data, 65505 bytes of zeroes
            00 * 65505

            # central directory
            #############################################

            504b 0102 # central file header signature (0x02014b50)
            1e03 # version made by, (798?)
            0a00 # version needed to extract (10)
            0000 # general purpose bit flag
            0000 # compression method (STORE)
            0000 # last mod file time
            0000 # last mod file date
            dddd 147d # CRC-32
            0d00 0000 # compressed size, 13 bytes
            0d00 0000 # uncompressed size, 13 bytes
            0800 # file name length, 8 bytes
            0000 # extra field length, 0 bytes
            0000 # file comment length
            0000 # disk number start
            0000 # internal file attributes
            0000 0000 # external file attributes
            0000 0000 # offset of local header

            6669 6c65 2e74 7874 # "file.txt"

            504b 0506 # end of central directory signature (0x06054b50)
            0000 # number of this disk
            0000 # number of the disk with the start of the central directory
            0100 # total number of entries in the central directory on this disk
            0100 # total number of entries in the central directory
            3600 0000 # size of the central directory, 76 bytes
            3200 0100 # offset of start of central directory
            0000 # file comment length
          """;

        public static void main(String[] args) throws IOException {
          var byteArray = parseHexString(hex);

          System.out.println("ZipInputStream entries:");
          try (var zipInputStream = new ZipInputStream(new java.io.ByteArrayInputStream(byteArray))) {
            var entry = zipInputStream.getNextEntry();
            while (entry != null) {
              printEntry(entry);
              entry = zipInputStream.getNextEntry();
            }
          }

          System.out.println("\nZipFile entries:");
          var out = Files.createTempFile("out", ".zip");
          try {
            Files.write(out, byteArray);
            try (var zipFile = new ZipFile(out.toFile())) {
              zipFile.entries().asIterator().forEachRemaining(Zip::printEntry);
            }
          } finally {
            Files.deleteIfExists(out);
          }
        }

        private static byte[] parseHexString(String hexString) {
          var bytes = hexString.lines()
            .map(line -> {
              var hexBytes = line.split("#", 2)[0].replace(" ", "");
              var repeat = hexBytes.split("\\*", 2);
              if (repeat.length == 2) {
                return repeat[0].repeat(Integer.parseInt(repeat[1]));
              } else {
                return hexBytes;
              }
            })
            .flatMap(byteString -> Stream.iterate(0, i -> i + 2)
              .limit(byteString.length() / 2)
              .map(off -> (byte)(Integer.parseInt(byteString.substring(off, off + 2), 16) & 0xff)))
            .collect(Collectors.toList());

          var byteArray = new byte[bytes.size()];
          IntStream.range(0, bytes.size()).forEach(i -> byteArray[i] = bytes.get(i));

          return byteArray;
        }

        private static void printEntry(ZipEntry entry) {
          System.out.println("name=" + entry.getName() +
            " size=" + entry.getSize() +
            " compressedSize=" + entry.getCompressedSize() +
            " crc=" + Long.toHexString(entry.getCrc()) +
            " extraSize=" + (entry.getExtra() == null ? 0 : entry.getExtra().length));
        }
      }

      ---------- END SOURCE ----------

            Unassigned Unassigned
            webbuggrp Webbug Group
            Votes:
            0 Vote for this issue
            Watchers:
            4 Start watching this issue

              Created:
              Updated:
              Resolved: