blob: 60b31ee2785eec88166ebc00cbafaef3da7e42d7 [file] [log] [blame]
/* ====================================================================
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==================================================================== */
package org.apache.poi.openxml4j.util;
import java.io.File;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;
import java.lang.reflect.Field;
import java.nio.charset.Charset;
import java.util.zip.InflaterInputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import org.apache.poi.util.POILogFactory;
import org.apache.poi.util.POILogger;
/**
* This class wraps a {@link ZipFile} in order to check the
* entries for <a href="https://en.wikipedia.org/wiki/Zip_bomb">zip bombs</a>
* while reading the archive.
* If a {@link ZipInputStream} is directly used, the wrapper
* can be applied via {@link #addThreshold(InputStream)}.
* The alert limits can be globally defined via {@link #setMaxEntrySize(long)}
* and {@link #setMinInflateRatio(double)}.
*/
public class ZipSecureFile extends ZipFile {
private static POILogger logger = POILogFactory.getLogger(ZipSecureFile.class);
private static double MIN_INFLATE_RATIO = 0.01d;
private static long MAX_ENTRY_SIZE = 0xFFFFFFFFl;
/**
* Sets the ratio between de- and inflated bytes to detect zipbomb.
* It defaults to 1% (= 0.01d), i.e. when the compression is better than
* 1% for any given read package part, the parsing will fail
*
* @param ratio the ratio between de- and inflated bytes to detect zipbomb
*/
public static void setMinInflateRatio(double ratio) {
MIN_INFLATE_RATIO = ratio;
}
/**
* Sets the maximum file size of a single zip entry. It defaults to 4GB,
* i.e. the 32-bit zip format maximum.
*
* @param maxEntrySize the max. file size of a single zip entry
*/
public static void setMaxEntrySize(long maxEntrySize) {
if (maxEntrySize < 0 || maxEntrySize > 0xFFFFFFFFl) {
throw new IllegalArgumentException("Max entry size is bounded [0-4GB].");
}
MAX_ENTRY_SIZE = maxEntrySize;
}
public ZipSecureFile(File file, int mode) throws IOException {
super(file, mode);
}
public ZipSecureFile(File file) throws ZipException, IOException {
super(file);
}
public ZipSecureFile(String name) throws IOException {
super(name);
}
/**
* Returns an input stream for reading the contents of the specified
* zip file entry.
*
* <p> Closing this ZIP file will, in turn, close all input
* streams that have been returned by invocations of this method.
*
* @param entry the zip file entry
* @return the input stream for reading the contents of the specified
* zip file entry.
* @throws ZipException if a ZIP format error has occurred
* @throws IOException if an I/O error has occurred
* @throws IllegalStateException if the zip file has been closed
*/
public InputStream getInputStream(ZipEntry entry) throws IOException {
InputStream zipIS = super.getInputStream(entry);
return addThreshold(zipIS);
}
public static ThresholdInputStream addThreshold(InputStream zipIS) throws IOException {
ThresholdInputStream newInner;
if (zipIS instanceof InflaterInputStream) {
try {
Field f = FilterInputStream.class.getDeclaredField("in");
f.setAccessible(true);
InputStream oldInner = (InputStream)f.get(zipIS);
newInner = new ThresholdInputStream(oldInner, null);
f.set(zipIS, newInner);
} catch (Exception ex) {
logger.log(POILogger.WARN, "SecurityManager doesn't allow manipulation via reflection for zipbomb detection - continue with original input stream", ex);
newInner = null;
}
} else {
// the inner stream is a ZipFileInputStream, i.e. the data wasn't compressed
newInner = null;
}
return new ThresholdInputStream(zipIS, newInner);
}
public static class ThresholdInputStream extends PushbackInputStream {
long counter = 0;
ThresholdInputStream cis;
public ThresholdInputStream(InputStream is, ThresholdInputStream cis) {
super(is,1);
this.cis = cis;
}
public int read() throws IOException {
int b = in.read();
if (b > -1) advance(1);
return b;
}
public int read(byte b[], int off, int len) throws IOException {
int cnt = in.read(b, off, len);
if (cnt > -1) advance(cnt);
return cnt;
}
public long skip(long n) throws IOException {
counter = 0;
return in.skip(n);
}
public synchronized void reset() throws IOException {
counter = 0;
in.reset();
}
public void advance(int advance) throws IOException {
counter += advance;
// check the file size first, in case we are working on uncompressed streams
if (counter < MAX_ENTRY_SIZE) {
if (cis == null) return;
double ratio = (double)cis.counter/(double)counter;
if (ratio > MIN_INFLATE_RATIO) return;
}
throw new IOException("Zip bomb detected! Exiting.");
}
public ZipEntry getNextEntry() throws IOException {
if (!(in instanceof ZipInputStream)) {
throw new UnsupportedOperationException("underlying stream is not a ZipInputStream");
}
counter = 0;
return ((ZipInputStream)in).getNextEntry();
}
public void closeEntry() throws IOException {
if (!(in instanceof ZipInputStream)) {
throw new UnsupportedOperationException("underlying stream is not a ZipInputStream");
}
counter = 0;
((ZipInputStream)in).closeEntry();
}
public void unread(int b) throws IOException {
if (!(in instanceof PushbackInputStream)) {
throw new UnsupportedOperationException("underlying stream is not a PushbackInputStream");
}
if (--counter < 0) counter = 0;
((PushbackInputStream)in).unread(b);
}
public void unread(byte[] b, int off, int len) throws IOException {
if (!(in instanceof PushbackInputStream)) {
throw new UnsupportedOperationException("underlying stream is not a PushbackInputStream");
}
counter -= len;
if (--counter < 0) counter = 0;
((PushbackInputStream)in).unread(b, off, len);
}
public int available() throws IOException {
return in.available();
}
public boolean markSupported() {
return in.markSupported();
}
public synchronized void mark(int readlimit) {
in.mark(readlimit);
}
}
}