Browse Source

Fix Zip Bomb attack security hotspot

DEVSIX-7103
pull/28/head
Angelina Pavlovets 3 years ago
parent
commit
7fa0001ca7
  1. 36
      itext.tests/itext.commons.tests/itext/commons/utils/ZipFileReaderTest.cs
  2. BIN
      itext.tests/itext.commons.tests/resources/itext/commons/utils/ZipFileReaderTest/zipBombTest.zip
  3. 42
      itext/itext.commons/itext/commons/logs/CommonsLogMessageConstant.cs
  4. 75
      itext/itext.commons/itext/commons/utils/ZipFileReader.cs
  5. 2
      port-hash

36
itext.tests/itext.commons.tests/itext/commons/utils/ZipFileReaderTest.cs

@ -24,7 +24,9 @@ using System;
using System.Collections.Generic;
using System.IO;
using iText.Commons.Exceptions;
using iText.Commons.Logs;
using iText.Test;
using iText.Test.Attributes;
namespace iText.Commons.Utils {
[NUnit.Framework.Category("UnitTest")]
@ -71,6 +73,40 @@ namespace iText.Commons.Utils {
}
}
[NUnit.Framework.Test]
[LogMessage(CommonsLogMessageConstant.UNCOMPRESSED_DATA_SIZE_IS_TOO_MUCH)]
public virtual void GetFileNamesFromZipBombBySettingThresholdSizeTest() {
using (ZipFileReader fileReader = new ZipFileReader(SOURCE_FOLDER + "zipBombTest.zip")) {
fileReader.SetThresholdRatio(1000);
fileReader.SetThresholdSize(10000);
ICollection<String> nameSet = fileReader.GetFileNames();
NUnit.Framework.Assert.IsNotNull(nameSet);
NUnit.Framework.Assert.AreEqual(0, nameSet.Count);
}
}
[NUnit.Framework.Test]
[LogMessage(CommonsLogMessageConstant.RATIO_IS_HIGHLY_SUSPICIOUS)]
public virtual void GetFileNamesFromZipBombBySettingThresholdRatioTest() {
using (ZipFileReader fileReader = new ZipFileReader(SOURCE_FOLDER + "zipBombTest.zip")) {
fileReader.SetThresholdRatio(5);
ICollection<String> nameSet = fileReader.GetFileNames();
NUnit.Framework.Assert.IsNotNull(nameSet);
NUnit.Framework.Assert.AreEqual(0, nameSet.Count);
}
}
[NUnit.Framework.Test]
[LogMessage(CommonsLogMessageConstant.TOO_MUCH_ENTRIES_IN_ARCHIVE)]
public virtual void GetFileNamesFromZipBombBySettingThresholdEntriesTest() {
using (ZipFileReader fileReader = new ZipFileReader(SOURCE_FOLDER + "archive.zip")) {
fileReader.SetThresholdEntries(5);
ICollection<String> nameSet = fileReader.GetFileNames();
NUnit.Framework.Assert.IsNotNull(nameSet);
NUnit.Framework.Assert.IsTrue(nameSet.Count <= 5);
}
}
[NUnit.Framework.Test]
public virtual void ReadFromZipWithNullPathTest() {
using (ZipFileReader reader = new ZipFileReader(SOURCE_FOLDER + "archive.zip")) {

BIN
itext.tests/itext.commons.tests/resources/itext/commons/utils/ZipFileReaderTest/zipBombTest.zip

42
itext/itext.commons/itext/commons/logs/CommonsLogMessageConstant.cs

@ -66,6 +66,36 @@ namespace iText.Commons.Logs {
/// </remarks>
public const String LOCAL_FILE_COMPRESSION_FAILED = "Cannot archive files into zip. " + "Exception message: {0}.";
/// <summary>
/// Message notifies that archive is suspicious to be a zip bomb due to large ratio between the compressed and
/// uncompressed archive entry.
/// </summary>
/// <remarks>
/// Message notifies that archive is suspicious to be a zip bomb due to large ratio between the compressed and
/// uncompressed archive entry.
/// <list type="bullet">
/// <item><description>0th is a threshold ratio;
/// </description></item>
/// </list>
/// </remarks>
public const String RATIO_IS_HIGHLY_SUSPICIOUS = "Ratio between compressed and uncompressed data is highly"
+ " suspicious, looks like a Zip Bomb Attack. Threshold ratio is {0}.";
/// <summary>
/// Message notifies that archive is suspicious to be a zip bomb because the number of file entries extracted from
/// the archive is greater than a predefined threshold.
/// </summary>
/// <remarks>
/// Message notifies that archive is suspicious to be a zip bomb because the number of file entries extracted from
/// the archive is greater than a predefined threshold.
/// <list type="bullet">
/// <item><description>0th is a threshold number of file entries in the archive;
/// </description></item>
/// </list>
/// </remarks>
public const String TOO_MUCH_ENTRIES_IN_ARCHIVE = "Too much entries in this archive, can lead to inodes "
+ "exhaustion of the system, looks like a Zip Bomb Attack. Threshold number of file entries is {0}.";
/// <summary>Message notifies that some exception has been thrown during json deserialization from object.</summary>
/// <remarks>
/// Message notifies that some exception has been thrown during json deserialization from object.
@ -92,6 +122,18 @@ namespace iText.Commons.Logs {
/// </remarks>
public const String UNABLE_TO_SERIALIZE_OBJECT = "Unable to serialize object. Exception {0} was thrown with the message: {1}.";
/// <summary>Message notifies that archive is suspicious to be a zip bomb due to large total size of the uncompressed data.
/// </summary>
/// <remarks>
/// Message notifies that archive is suspicious to be a zip bomb due to large total size of the uncompressed data.
/// <list type="bullet">
/// <item><description>0th is a threshold size;
/// </description></item>
/// </list>
/// </remarks>
public const String UNCOMPRESSED_DATA_SIZE_IS_TOO_MUCH = "The uncompressed data size is too much for the"
+ " application resource capacity, looks like a Zip Bomb Attack. Threshold size is {0}.";
/// <summary>
/// Message notifies that unknown placeholder was ignored during parsing of the producer line
/// format.

75
itext/itext.commons/itext/commons/utils/ZipFileReader.cs

@ -26,20 +26,26 @@ using System.IO;
using System.IO.Compression;
using System.Text;
using iText.Commons.Exceptions;
using iText.Commons.Logs;
using Microsoft.Extensions.Logging;
namespace iText.Commons.Utils {
/// <summary>Allows reading entries from a zip file.</summary>
public class ZipFileReader : IDisposable {
private readonly ZipArchive zipArchive;
private static readonly ILogger LOGGER = ITextLogManager.GetLogger(typeof(ZipFileReader));
private readonly ZipArchive zipArchive;
private int thresholdSize = 1_000_000_000;
private int thresholdEntries = 10000;
private double thresholdRatio = 10;
/// <summary>
/// Creates an instance for zip file reading.
/// </summary>
/// <param name="archivePath">the path to the zip file to read</param>
/// <exception cref="IOException">if some I/O exception occurs</exception>
public ZipFileReader(String archivePath)
{
public ZipFileReader(String archivePath) {
if (archivePath == null) {
throw new IOException(CommonsExceptionMessageConstant.FILE_NAME_CAN_NOT_BE_NULL);
}
@ -52,9 +58,44 @@ namespace iText.Commons.Utils {
/// Get all file entries paths inside the reading zip file.
/// </summary>
/// <returns>the {@link Set} of all file entries paths</returns>
/// <exception cref="IOException">if some I/O exception occurs</exception>
public ISet<String> GetFileNames() {
ISet<String> fileNames = new HashSet<String>();
int totalSizeArchive = 0;
int totalEntryArchive = 0;
foreach (ZipArchiveEntry entry in zipArchive.Entries) {
Boolean zipBombSuspicious = false;
totalEntryArchive++;
using (Stream stream = entry.Open()) {
int nBytes;
byte[] buffer = new byte[2048];
int totalSizeEntry = 0;
while ((nBytes = stream.Read(buffer, 0, 2048)) > 0) {
totalSizeEntry += nBytes;
totalSizeArchive += nBytes;
double compressionRatio = (double) totalSizeEntry / entry.CompressedLength;
if (compressionRatio > thresholdRatio) {
zipBombSuspicious = true;
break;
}
}
if (zipBombSuspicious) {
LOGGER.LogWarning(MessageFormatUtil.Format(CommonsLogMessageConstant.RATIO_IS_HIGHLY_SUSPICIOUS,
thresholdRatio));
break;
}
if (totalSizeArchive > thresholdSize) {
LOGGER.LogWarning(MessageFormatUtil.Format(CommonsLogMessageConstant.UNCOMPRESSED_DATA_SIZE_IS_TOO_MUCH,
thresholdSize));
break;
}
if (totalEntryArchive > thresholdEntries) {
LOGGER.LogWarning(MessageFormatUtil.Format(CommonsLogMessageConstant.TOO_MUCH_ENTRIES_IN_ARCHIVE,
thresholdEntries));
break;
}
}
String entryName = entry.FullName;
if (!IsDirectory(entryName)) {
fileNames.Add(entryName);
@ -81,6 +122,32 @@ namespace iText.Commons.Utils {
return entry.Open();
}
/// <summary>
/// Sets the maximum total uncompressed data size to prevent a Zip Bomb Attack.
/// Default value is 1 GB (1000000000).
/// </summary>
/// <param name="thresholdSize"> the threshold for maximum total size of the uncompressed data</param>
public void SetThresholdSize(int thresholdSize) {
this.thresholdSize = thresholdSize;
}
/// <summary>
/// Sets the maximum number of file entries in the archive to prevent a Zip Bomb Attack. Default value is 10000.
/// </summary>
/// <param name="thresholdEntries"> maximum number of file entries in the archive</param>
public void SetThresholdEntries(int thresholdEntries) {
this.thresholdEntries = thresholdEntries;
}
/// <summary>
/// Sets the maximum ratio between compressed and uncompressed data to prevent a Zip Bomb Attack. In general
/// the data compression ratio for most of the legit archives is 1 to 3. Default value is 10.
/// </summary>
/// <param name="thresholdRatio"> maximum ratio between compressed and uncompressed data</param>
public void SetThresholdRatio(double thresholdRatio) {
this.thresholdRatio = thresholdRatio;
}
public void Dispose() {
zipArchive.Dispose();
}

2
port-hash

@ -1 +1 @@
7f0e11025337dd5dfccbc7a590c8d0ba95b1a871
739cba5c8643a5dffb43bfa7bd791340c72b5b1e
Loading…
Cancel
Save