ZipInputStream не может проанализировать zip-бомбу объемом 281 ТБ

#java

Вопрос:

У меня есть этот простой классный скрипт, который перечисляет записи в zip-файлах вместе с их размером несжатого файла

 #!/usr/bin/env groovy

import groovy.transform.CompileStatic
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.nio.file.Path
import java.nio.file.Files

@CompileStatic
long zipSize(InputStream is) {
    long totalSize = 0
    byte[] buffer = new byte[0x10000]
    new ZipInputStream(is).withCloseable { zis ->
        while(true) {
            ZipEntry zipEntry = zis.nextEntry
            if(zipEntry == null) break
            long entrySize = 0
            while(true) {
                int read = zis.read(buffer)
                if(read <= 0) break
                totalSize  = read
                entrySize  = read
            }
            zis.closeEntry()
            println("Uncompressed size of ${zipEntry.name} is ${entrySize} bytes")
        }
    }
    return totalSize
}

args.each {
    println("Opening '$it'")
    Path path = Path.of(it)
    long size = zipSize(Files.newInputStream(path))
    println("Total uncompressed size of '$it' is $size bytes")
}
 

Я попытался запустить его с помощью zblg.zip который является zip-бомбой, загруженной отсюда, файл должен содержать несколько тысяч записей, каждая из которых имеет размер около 4 ГБ, однако вывод сценария просто:

 Opening 'Downloads/zblg.zip'
Uncompressed size of 0 is 4294967240 bytes
Total uncompressed size of 'Downloads/zblg.zip' is 4294967240 bytes
 

это означает, что читается только первая запись, программа не показывает никаких ошибок и не создает никаких исключений, она просто действует так, как если бы zip-архив содержал только одну запись… Есть ли какие-либо ограничения, о ZipInputStream которых я не знаю или которые я не могу найти в документации?

Редактировать:

Я также собрал пример Java 8, чтобы воспроизвести проблему

 import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Paths;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.nio.file.Path;
import java.nio.file.Files;


public class zipSize {
    private static long zipSize(InputStream is) throws IOException {
        long totalSize = 0;
        byte[] buffer = new byte[0x10000];
        try(ZipInputStream zis = new ZipInputStream(is)) {
            while(true) {
                ZipEntry zipEntry = zis.getNextEntry();
                if(zipEntry == null) break;
                long entrySize = 0;
                while(true) {
                    int read = zis.read(buffer);
                    if(read <= 0) break;
                    entrySize  = read;
                }
                totalSize  = entrySize;
                zis.closeEntry();
                System.out.printf("Uncompressed size of '%s' is %d bytesn", zipEntry.getName(), entrySize);
            }
        }
        return totalSize;
    }

    public static void main(String[] args) throws IOException {
        for(String arg : args) {
            System.out.printf("Opening '%s'n", arg);
            Path path = Paths.get(arg);
            long size = zipSize(Files.newInputStream(path));
            System.out.printf("Total uncompressed size of '%s' is %d bytesn", arg, size);
        }
    }
}
 

Комментарии:

1. Внутренний цикл while можно было бы на данный момент прокомментировать и просто использовать zipEntry.getSize() .

2. хорошо, справедливо … но это не имеет никакого значения с точки зрения фактического результата

3. Вместо физического чтения блоков, просто переход к следующему почтовому индексу должен почти сработать, учитывая правильные сжатые размеры.

4. Я попробовал это и, к сожалению, не заметил никакой разницы

5. Я думаю, что в zip-файлах в конце есть своего рода таблица содержимого, так что вам не нужно сканировать весь файл, чтобы просто перечислить содержимое, поэтому размер конкретной записи не должен быть таким важным (но я не уверен на 100% в этом).

Ответ №1:

Zip — файл содержит своего рода таблицу содержимого со списком всех его записей под названием «центральный каталог» в конце архива.

Проблема в том , что java.util.zip.ZipInputStream , будучи потоком, он не может прочитать его, пока его обязанности не будут уже выполнены.

Чтобы обойти эту проблему, он ищет локальный файл заголовка, часть метаданных начиная с магией 0x04034b50 число (источник здесь), который предваряет каждый элемент молнии, и он построен на предположении, что после каждой молнии ввода данных, там сразу новый локальный файл заголовка; если это не так, то ZipInputstream.getNextEntry() возвращает пустую запись, которая, как он обычно сигналы о том, что все записи были прочитаны.

К сожалению, допускается наличие бессмысленных данных между одной записью и следующей или даже записей, которые больше не являются частью архива. Этот последний случай был на самом деле очень распространен в прошлом, когда zip-файлы записывались на несколько дискет, и физическое удаление записи в начале архива потребовало бы повторной вставки всех дисков по одному, чтобы переместить следующие записи и сжать архив. Поскольку это было явно непрактично, оптимизация заключалась в том, чтобы просто удалить запись из центрального каталога в конце файла, оставив заголовок локального файла и фактическое содержимое записи на месте.

В результате этого, java.util.zip.ZipInputStream являются недостоверными, так как это может привести к ошибочным результатам (призрак записи и/или отсутствует архиве записях), однако ни один из этих вопросов будет, если вы можете с уверенностью предположить, что файл zip был создан для Java стандартная библиотека сама, используя для Java.утиль.на молнии.ZipOutputStream, а не оставлять пустые места между записями и записывает только реальные (что также представляется в Центральный каталог) записи.

По этой причине Zip-файлы, как правило, не подходят для потоковых приложений, поскольку единственный надежный способ их чтения-это сначала прочитать центральный каталог в конце файла (что java.util.zip.ZipFile делает и, как таковое, не влияет на эту проблему).

Более подробная информация здесь

Комментарии:

1. однако пропуск записей (с чем я здесь столкнулся) не указан на этой странице между возможными симптомами