Skip to content

Commit 739cba5

Browse files
committed
Fix Zip Bomb attack security hotspot
DEVSIX-7103
1 parent 7f0e110 commit 739cba5

File tree

4 files changed

+146
-2
lines changed

4 files changed

+146
-2
lines changed

commons/src/main/java/com/itextpdf/commons/logs/CommonsLogMessageConstant.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,28 @@ public final class CommonsLogMessageConstant {
6767
public static final String LOCAL_FILE_COMPRESSION_FAILED = "Cannot archive files into zip. "
6868
+ "Exception message: {0}.";
6969

70+
/**
71+
* Message notifies that archive is suspicious to be a zip bomb due to large ratio between the compressed and
72+
* uncompressed archive entry.
73+
*
74+
* <ul>
75+
* <li>0th is a threshold ratio;
76+
* </ul>
77+
*/
78+
public static final String RATIO_IS_HIGHLY_SUSPICIOUS = "Ratio between compressed and uncompressed data is highly"
79+
+ " suspicious, looks like a Zip Bomb Attack. Threshold ratio is {0}.";
80+
81+
/**
82+
* Message notifies that archive is suspicious to be a zip bomb because the number of file entries extracted from
83+
* the archive is greater than a predefined threshold.
84+
*
85+
* <ul>
86+
* <li>0th is a threshold number of file entries in the archive;
87+
* </ul>
88+
*/
89+
public static final String TOO_MUCH_ENTRIES_IN_ARCHIVE = "Too much entries in this archive, can lead to inodes "
90+
+ "exhaustion of the system, looks like a Zip Bomb Attack. Threshold number of file entries is {0}.";
91+
7092
/**
7193
* Message notifies that some exception has been thrown during json deserialization from object.
7294
* List of params:
@@ -91,6 +113,16 @@ public final class CommonsLogMessageConstant {
91113
public static final String UNABLE_TO_SERIALIZE_OBJECT =
92114
"Unable to serialize object. Exception {0} was thrown with the message: {1}.";
93115

116+
/**
117+
* Message notifies that archive is suspicious to be a zip bomb due to large total size of the uncompressed data.
118+
*
119+
* <ul>
120+
* <li>0th is a threshold size;
121+
* </ul>
122+
*/
123+
public static final String UNCOMPRESSED_DATA_SIZE_IS_TOO_MUCH = "The uncompressed data size is too much for the"
124+
+ " application resource capacity, looks like a Zip Bomb Attack. Threshold size is {0}.";
125+
94126
/**
95127
* Message notifies that unknown placeholder was ignored during parsing of the producer line
96128
* format. List of params:

commons/src/main/java/com/itextpdf/commons/utils/ZipFileReader.java

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ This file is part of the iText (R) project.
2323
package com.itextpdf.commons.utils;
2424

2525
import com.itextpdf.commons.exceptions.CommonsExceptionMessageConstant;
26+
import com.itextpdf.commons.logs.CommonsLogMessageConstant;
2627

28+
import java.io.BufferedInputStream;
2729
import java.io.Closeable;
2830
import java.io.IOException;
2931
import java.io.InputStream;
@@ -33,14 +35,21 @@ This file is part of the iText (R) project.
3335
import java.util.Set;
3436
import java.util.zip.ZipEntry;
3537
import java.util.zip.ZipFile;
38+
import org.slf4j.Logger;
39+
import org.slf4j.LoggerFactory;
3640

3741
/**
3842
* Allows reading entries from a zip file.
3943
*/
4044
public class ZipFileReader implements Closeable {
45+
private static final Logger LOGGER = LoggerFactory.getLogger(ZipFileReader.class);
4146

4247
private final ZipFile zipFile;
4348

49+
private int thresholdSize = 1_000_000_000;
50+
private int thresholdEntries = 10000;
51+
private double thresholdRatio = 10;
52+
4453
/**
4554
* Creates an instance for zip file reading.
4655
*
@@ -59,13 +68,49 @@ public ZipFileReader(String archivePath) throws IOException {
5968
* Get all file entries paths inside the reading zip file.
6069
*
6170
* @return the {@link Set} of all file entries paths
71+
*
72+
* @throws IOException if some I/O exception occurs
6273
*/
63-
public Set<String> getFileNames() {
74+
public Set<String> getFileNames() throws IOException {
6475
final Set<String> fileNames = new HashSet<>();
6576

6677
final Enumeration<? extends ZipEntry> entries = zipFile.entries();
78+
79+
int totalSizeArchive = 0;
80+
int totalEntryArchive = 0;
6781
while (entries.hasMoreElements()) {
6882
ZipEntry entry = entries.nextElement();
83+
boolean zipBombSuspicious = false;
84+
try (InputStream in = new BufferedInputStream(zipFile.getInputStream(entry))) {
85+
totalEntryArchive++;
86+
int nBytes;
87+
byte[] buffer = new byte[2048];
88+
int totalSizeEntry = 0;
89+
while ((nBytes = in.read(buffer)) > 0) {
90+
totalSizeEntry += nBytes;
91+
totalSizeArchive += nBytes;
92+
double compressionRatio = (double) totalSizeEntry / entry.getCompressedSize();
93+
if (compressionRatio > thresholdRatio) {
94+
zipBombSuspicious = true;
95+
break;
96+
}
97+
}
98+
if (zipBombSuspicious) {
99+
LOGGER.warn(MessageFormatUtil.format(CommonsLogMessageConstant.RATIO_IS_HIGHLY_SUSPICIOUS,
100+
thresholdRatio));
101+
break;
102+
}
103+
if (totalSizeArchive > thresholdSize) {
104+
LOGGER.warn(MessageFormatUtil.format(CommonsLogMessageConstant.UNCOMPRESSED_DATA_SIZE_IS_TOO_MUCH,
105+
thresholdSize));
106+
break;
107+
}
108+
if (totalEntryArchive > thresholdEntries) {
109+
LOGGER.warn(MessageFormatUtil.format(CommonsLogMessageConstant.TOO_MUCH_ENTRIES_IN_ARCHIVE,
110+
thresholdEntries));
111+
break;
112+
}
113+
}
69114
if (!entry.isDirectory()) {
70115
fileNames.add(entry.getName());
71116
}
@@ -94,6 +139,34 @@ public InputStream readFromZip(String fileName) throws IOException {
94139
return zipFile.getInputStream(entry);
95140
}
96141

142+
/**
143+
* Sets the maximum total uncompressed data size to prevent a Zip Bomb Attack. Default value is 1 GB (1000000000).
144+
*
145+
* @param thresholdSize the threshold for maximum total size of the uncompressed data
146+
*/
147+
public void setThresholdSize(int thresholdSize) {
148+
this.thresholdSize = thresholdSize;
149+
}
150+
151+
/**
152+
* Sets the maximum number of file entries in the archive to prevent a Zip Bomb Attack. Default value is 10000.
153+
*
154+
* @param thresholdEntries maximum number of file entries in the archive
155+
*/
156+
public void setThresholdEntries(int thresholdEntries) {
157+
this.thresholdEntries = thresholdEntries;
158+
}
159+
160+
/**
161+
* Sets the maximum ratio between compressed and uncompressed data to prevent a Zip Bomb Attack. In general
162+
* the data compression ratio for most of the legit archives is 1 to 3. Default value is 10.
163+
*
164+
* @param thresholdRatio maximum ratio between compressed and uncompressed data
165+
*/
166+
public void setThresholdRatio(double thresholdRatio) {
167+
this.thresholdRatio = thresholdRatio;
168+
}
169+
97170
@Override
98171
public void close() throws IOException {
99172
zipFile.close();

commons/src/test/java/com/itextpdf/commons/utils/ZipFileReaderTest.java

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,15 @@ This file is part of the iText (R) project.
2323
package com.itextpdf.commons.utils;
2424

2525
import com.itextpdf.commons.exceptions.CommonsExceptionMessageConstant;
26+
import com.itextpdf.commons.logs.CommonsLogMessageConstant;
2627
import com.itextpdf.test.ExtendedITextTest;
28+
import com.itextpdf.test.annotations.LogMessage;
29+
import com.itextpdf.test.annotations.LogMessages;
2730
import com.itextpdf.test.annotations.type.UnitTest;
2831

2932
import java.io.ByteArrayOutputStream;
3033
import java.io.IOException;
3134
import java.io.InputStream;
32-
import java.nio.charset.StandardCharsets;
3335
import java.util.Set;
3436
import org.junit.Assert;
3537
import org.junit.Test;
@@ -80,6 +82,43 @@ public void getFileNamesFromZipTest() throws IOException {
8082
}
8183
}
8284

85+
@Test
86+
@LogMessages(messages = @LogMessage(messageTemplate = CommonsLogMessageConstant.UNCOMPRESSED_DATA_SIZE_IS_TOO_MUCH))
87+
public void getFileNamesFromZipBombBySettingThresholdSizeTest() throws IOException {
88+
try (ZipFileReader fileReader = new ZipFileReader(SOURCE_FOLDER + "zipBombTest.zip")) {
89+
fileReader.setThresholdRatio(1000);
90+
fileReader.setThresholdSize(10000);
91+
Set<String> nameSet = fileReader.getFileNames();
92+
93+
Assert.assertNotNull(nameSet);
94+
Assert.assertEquals(0, nameSet.size());
95+
}
96+
}
97+
98+
@Test
99+
@LogMessages(messages = @LogMessage(messageTemplate = CommonsLogMessageConstant.RATIO_IS_HIGHLY_SUSPICIOUS))
100+
public void getFileNamesFromZipBombBySettingThresholdRatioTest() throws IOException {
101+
try (ZipFileReader fileReader = new ZipFileReader(SOURCE_FOLDER + "zipBombTest.zip")) {
102+
fileReader.setThresholdRatio(5);
103+
Set<String> nameSet = fileReader.getFileNames();
104+
105+
Assert.assertNotNull(nameSet);
106+
Assert.assertEquals(0, nameSet.size());
107+
}
108+
}
109+
110+
@Test
111+
@LogMessages(messages = @LogMessage(messageTemplate = CommonsLogMessageConstant.TOO_MUCH_ENTRIES_IN_ARCHIVE))
112+
public void getFileNamesFromZipBombBySettingThresholdEntriesTest() throws IOException {
113+
try (ZipFileReader fileReader = new ZipFileReader(SOURCE_FOLDER + "archive.zip")) {
114+
fileReader.setThresholdEntries(5);
115+
Set<String> nameSet = fileReader.getFileNames();
116+
117+
Assert.assertNotNull(nameSet);
118+
Assert.assertTrue(nameSet.size() <= 5);
119+
}
120+
}
121+
83122
@Test
84123
public void readFromZipWithNullPathTest() throws IOException {
85124
try (ZipFileReader reader = new ZipFileReader(SOURCE_FOLDER + "archive.zip")) {

0 commit comments

Comments
 (0)