/* ==================================================================== | |
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); | |
} | |
} | |
} |