aboutsummaryrefslogtreecommitdiffstats
path: root/src/ooxml/java/org/apache/poi/openxml4j/util/ZipSecureFile.java
blob: 5423ea4d475603450c80794aba8cb8d69ce58cb7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
/* ====================================================================
   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.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 indicating a 
     * Zip-Bomb.
     *
     * @param ratio the ratio between de- and inflated bytes to detect zipbomb
     */
    public static void setMinInflateRatio(double ratio) {
        MIN_INFLATE_RATIO = ratio;
    }
    
    /**
     * Returns the current minimum compression rate that is used.
     * 
     * See setMinInflateRatio() for details.
     *
     * @return The min accepted compression-ratio.  
     */
    public static double getMinInflateRatio() {
        return MIN_INFLATE_RATIO;
    }

    /**
     * Sets the maximum file size of a single zip entry. It defaults to 4GB,
     * i.e. the 32-bit zip format maximum.
     * 
     * This can be used to limit memory consumption and protect against 
     * security vulnerabilities when documents are provided by users.
     *
     * @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;
    }

    /**
     * Returns the current maximum allowed uncompressed file size.
     * 
     * See setMaxEntrySize() for details.
     *
     * @return The max accepted uncompressed file size. 
     */
    public static long getMaxEntrySize() {
        return MAX_ENTRY_SIZE;
    }

    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! The file would exceed certain limits which usually indicate that the file is used to inflate memory usage and thus could pose a security risk. "
                    + "You can adjust these limits via setMinInflateRatio() and setMaxEntrySize() if you need to work with files which exceed these limits. "
                    + "Counter: " + counter + ", cis.counter: " + (cis == null ? 0 : cis.counter) + ", ratio: " + (cis == null ? 0 : ((double)cis.counter)/counter)
                    + "Limits: MIN_INFLATE_RATIO: " + MIN_INFLATE_RATIO + ", MAX_ENTRY_SIZE: " + MAX_ENTRY_SIZE);
        }

        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);
        }
    }
}