POI安全-压缩炸弹

今天用poi操作xlsx文件时,报错如下

18:49:22.124 [main] ERROR org.apache.poi.openxml4j.opc.internal.marshallers.ZipPartMarshaller - Cannot write: /xl/media/image1.emf: in ZIP
java.io.IOException: Zip bomb detected! The file would exceed the max. ratio of compressed file size to the size of the expanded data.
This may indicate that the file is used to inflate memory usage and thus could pose a security risk.
You can adjust this limit via ZipSecureFile.setMinInflateRatio() if you need to work with files which exceed this limit.
Uncompressed size: 106496, Raw/compressed size: 512, ratio: 0.004808
Limits: MIN_INFLATE_RATIO: 0.010000, Entry: xl/media/image1.emf
	at org.apache.poi.openxml4j.util.ZipArchiveThresholdInputStream.checkThreshold(ZipArchiveThresholdInputStream.java:143)
	at org.apache.poi.openxml4j.util.ZipArchiveThresholdInputStream.read(ZipArchiveThresholdInputStream.java:82)
Exception in thread "main" org.apache.poi.openxml4j.exceptions.OpenXML4JRuntimeException: Fail to save: an error occurs while saving the package : The part /xl/media/image1.emf failed to be saved in the stream with marshaller org.apache.poi.openxml4j.opc.internal.marshallers.DefaultMarshaller@49dc7102. Enable logging via Log4j 2 for more details.
	at org.apache.poi.openxml4j.opc.ZipPackage.saveImpl(ZipPackage.java:598)
	at org.apache.poi.openxml4j.opc.OPCPackage.save(OPCPackage.java:1490)
	at org.apache.poi.ooxml.POIXMLDocument.write(POIXMLDocument.java:227)
	at com.szh.demo.common.util.PoiUtils.main(PoiUtils.java:191)
Caused by: org.apache.poi.openxml4j.exceptions.OpenXML4JException: The part /xl/media/image1.emf failed to be saved in the stream with marshaller org.apache.poi.openxml4j.opc.internal.marshallers.DefaultMarshaller@49dc7102. Enable logging via Log4j 2 for more details.
	at org.apache.poi.openxml4j.opc.ZipPackage.saveImpl(ZipPackage.java:587)

翻译如下:

18:49:22.124 [main] 错误 org.apache.poi.openxml4j.opc.internal.marshallers.ZipPartMarshaller - 无法写入:/xl/media/image1.emf:ZIP
java.io.IOException:检测到压缩炸弹! 该文件超过压缩文件大小与扩展数据大小之比的最大值。
这可能表明该文件用于增加内存使用量,因此可能会带来安全风险。
如果您需要处理超过此限制的文件,您可以通过 ZipSecureFile.setMinInflateRatio() 调整此限制。
未压缩大小:106496,原始/压缩大小:512,比率:0.004808
限制:MIN_INFLATE_RATIO:0.010000,实体:xl/media/image1.emf
在 org.apache.poi.openxml4j.util.ZipArchiveThresholdInputStream.checkThreshold(ZipArchiveThresholdInputStream.java:143)org.apache.poi.openxml4j.util.ZipArchiveThresholdInputStream.read(ZipArchiveThresholdInputStream.java:82)
Exception in thread "main" org.apache.poi.openxml4j.exceptions.OpenXML4JRuntimeException: 保存失败:保存包时出错:The part /xl/media/image1.emf failed to be saved in the stream with marshaller org.apache.poi.openxml4j.opc.internal.marshallers.DefaultMarshaller@49dc7102. Enable logging via Log4j 2 for more details.
	at org.apache.poi.openxml4j.opc.ZipPackage.saveImpl(ZipPackage.java:598)
	at org.apache.poi.openxml4j.opc.OPCPackage.save(OPCPackage.java:1490)
	at org.apache.poi.ooxml.POIXMLDocument.write(POIXMLDocument.java:227)
	at com.szh.demo.common.util.PoiUtils.main(PoiUtils.java:191)
Caused by: org.apache.poi.openxml4j.exceptions.OpenXML4JException: The part /xl/media/image1.emf failed to be saved in the stream with marshaller org.apache.poi.openxml4j.opc.internal.marshallers.DefaultMarshaller@49dc7102. Enable logging via Log4j 2 for more details.
	at org.apache.poi.openxml4j.opc.ZipPackage.saveImpl(ZipPackage.java:587)

按照提示,我加入了一行代码

ZipSecureFile.setMinInflateRatio(0.001);
public static void main(String[] args) throws Exception {
    	ZipSecureFile.setMinInflateRatio(0.001);
		XSSFWorkbook workbook = new XSSFWorkbook("D:/ceshi/009.xlsx");
    	FileOutputStream outputStream = new FileOutputStream(new File("D:/ceshi/76543.xlsx"));
    	workbook.write(outputStream);
    	workbook.close();
    	outputStream.close();
	}

确实成功了。

用7-zip打开源文件
POI安全-压缩炸弹_第1张图片
果然压缩率惊人 123522 / 3992712 = 0.0309,这还不算载入到内存中的大小

附上poi源码
@see { org.apache.poi.openxml4j.util.ZipArchiveThresholdInputStream.checkThreshold() }

package org.apache.poi.openxml4j.util;

import static org.apache.poi.openxml4j.util.ZipSecureFile.MAX_ENTRY_SIZE;
import static org.apache.poi.openxml4j.util.ZipSecureFile.MIN_INFLATE_RATIO;

import java.io.EOFException;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Locale;
import java.util.zip.ZipException;

import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
import org.apache.commons.compress.utils.InputStreamStatistics;
import org.apache.poi.openxml4j.exceptions.NotOfficeXmlFileException;
import org.apache.poi.util.IOUtils;
import org.apache.poi.util.Internal;

@Internal
public class ZipArchiveThresholdInputStream extends FilterInputStream {
    // don't alert for expanded sizes smaller than 100k
    private static final long GRACE_ENTRY_SIZE = 100*1024L;

    private static final String MAX_ENTRY_SIZE_MSG =
        "Zip bomb detected! The file would exceed the max size of the expanded data in the zip-file.\n" +
        "This may indicates that the file is used to inflate memory usage and thus could pose a security risk.\n" +
        "You can adjust this limit via ZipSecureFile.setMaxEntrySize() if you need to work with files which are very large.\n" +
        "Uncompressed size: %d, Raw/compressed size: %d\n" +
        "Limits: MAX_ENTRY_SIZE: %d, Entry: %s";

    private static final String MIN_INFLATE_RATIO_MSG =
        "Zip bomb detected! The file would exceed the max. ratio of compressed file size to the size of the expanded data.\n" +
        "This may indicate that the file is used to inflate memory usage and thus could pose a security risk.\n" +
        "You can adjust this limit via ZipSecureFile.setMinInflateRatio() if you need to work with files which exceed this limit.\n" +
        "Uncompressed size: %d, Raw/compressed size: %d, ratio: %f\n" +
        "Limits: MIN_INFLATE_RATIO: %f, Entry: %s";

    /**
     * the reference to the current entry is only used for a more detailed log message in case of an error
     */
    private ZipArchiveEntry entry;
    private boolean guardState = true;

    public ZipArchiveThresholdInputStream(InputStream is) {
        super(is);
        if (!(is instanceof InputStreamStatistics)) {
            throw new IllegalArgumentException("InputStream of class "+is.getClass()+" is not implementing InputStreamStatistics.");
        }
    }

    @Override
    public int read() throws IOException {
        int b = super.read();
        if (b > -1) {
            checkThreshold();
        }
        return b;
    }

    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        int cnt = super.read(b, off, len);
        if (cnt > -1) {
            checkThreshold();
        }
        return cnt;
    }

    @Override
    public long skip(long n) throws IOException {
        long cnt = IOUtils.skipFully(super.in, n);
        if (cnt > 0) {
            checkThreshold();
        }
       return cnt;
    }

    /**
     * De-/activate threshold check.
     * A disabled guard might make sense, when POI is processing its own temporary data (see #59743)
     *
     * @param guardState {@code true} (= default) enables the threshold check
     */
    public void setGuardState(boolean guardState) {
        this.guardState = guardState;
    }

    private void checkThreshold() throws IOException {
        if (!guardState) {
            return;
        }

        final InputStreamStatistics stats = (InputStreamStatistics)in;
        final long payloadSize = stats.getUncompressedCount();

        long rawSize;
        try {
            rawSize = stats.getCompressedCount();
        } catch (NullPointerException e) {
            // this can happen with a very specially crafted file
            // see https://issues.apache.org/jira/browse/COMPRESS-598 for a related bug-report
            // therefore we try to handle this gracefully for now
            // this try/catch can be removed when COMPRESS-598 is fixed
            rawSize = 0;
        }

        final String entryName = entry == null ? "not set" : entry.getName();

        // check the file size first, in case we are working on uncompressed streams
        if(payloadSize > MAX_ENTRY_SIZE) {
            throw new IOException(String.format(Locale.ROOT, MAX_ENTRY_SIZE_MSG, payloadSize, rawSize, MAX_ENTRY_SIZE, entryName));
        }

        // don't alert for small expanded size
        if (payloadSize <= GRACE_ENTRY_SIZE) {
            return;
        }

        double ratio = rawSize / (double)payloadSize;
        if (ratio >= MIN_INFLATE_RATIO) {
            return;
        }

        // one of the limits was reached, report it
        throw new IOException(String.format(Locale.ROOT, MIN_INFLATE_RATIO_MSG, payloadSize, rawSize, ratio, MIN_INFLATE_RATIO, entryName));
    }

    ZipArchiveEntry getNextEntry() throws IOException {
        if (!(in instanceof ZipArchiveInputStream)) {
            throw new IllegalStateException("getNextEntry() is only allowed for stream based zip processing.");
        }

        try {
            entry = ((ZipArchiveInputStream) in).getNextZipEntry();
            return entry;
        } catch (ZipException ze) {
            if (ze.getMessage().startsWith("Unexpected record signature")) {
                throw new NotOfficeXmlFileException(
                        "No valid entries or contents found, this is not a valid OOXML (Office Open XML) file", ze);
            }
            throw ze;
        } catch (EOFException e) {
            return null;
        }
    }

    /**
     * Sets the zip entry for a detailed logging
     * @param entry the entry
     */
    void setEntry(ZipArchiveEntry entry) {
        this.entry = entry;
    }
}

如果你想用poi操作xlsx文件,建议不要在xlsx文件中插入emf格式的图片,因为这家伙压缩率太夸张,poi会当成压缩炸弹。

附上《测试文件》

你可能感兴趣的:(java,poi,xlsx,压缩炸弹)