changeset 6664:14eabda1f718 machine-spirits

fix possible memory exhaustion via forged gzip metadata
author Sebastien Jodogne <s.jodogne@gmail.com>
date Mon, 23 Mar 2026 07:54:13 +0100
parents c00c3d82289c
children f2b978accde2
files NEWS OrthancFramework/Sources/Compression/GzipCompressor.cpp OrthancFramework/Sources/Compression/ZipReader.cpp
diffstat 3 files changed, 91 insertions(+), 61 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Sun Mar 22 11:26:21 2026 +0100
+++ b/NEWS	Mon Mar 23 07:54:13 2026 +0100
@@ -65,6 +65,7 @@
   - Fix possible out-of-bound access when reading a DICOM file with invalid group length tag.
   - Fix possible memory exhaustion via very large "Content-Length"
   - Fix possible memory exhaustion via forged ZIP metadata
+  - Fix possible memory exhaustion via forged gzip metadata
 * Upgraded dependencies for static builds:
   - boost 1.89.0
   - dcmtk 3.7.0
--- a/OrthancFramework/Sources/Compression/GzipCompressor.cpp	Sun Mar 22 11:26:21 2026 +0100
+++ b/OrthancFramework/Sources/Compression/GzipCompressor.cpp	Mon Mar 23 07:54:13 2026 +0100
@@ -29,6 +29,7 @@
 #include <string.h>
 #include <zlib.h>
 
+#include "../ChunkedBuffer.h"
 #include "../OrthancException.h"
 #include "../Logging.h"
 
@@ -177,100 +178,128 @@
   }
 
 
+  namespace
+  {
+    class GzipRaii : public boost::noncopyable
+    {
+    private:
+      z_stream&  stream_;
+
+    public:
+      GzipRaii(z_stream& stream) :
+        stream_(stream)
+      {
+        int error = inflateInit2(&stream_,
+                                 MAX_WBITS + 16);  // this is a gzip input
+
+        if (error != Z_OK)
+        {
+          throw OrthancException(ErrorCode_InternalError, "Cannot initialize zlib");
+        }
+      }
+
+      ~GzipRaii()
+      {
+        inflateEnd(&stream_);
+      }
+    };
+  }
+
+
   void GzipCompressor::Uncompress(std::string& uncompressed,
                                   const void* compressed,
                                   size_t compressedSize)
   {
-    uint64_t uncompressedSize;
     const uint8_t* source = reinterpret_cast<const uint8_t*>(compressed);
 
+    bool hasMaximumSize = false;
+    size_t maximumSize = 0;
+
     if (HasPrefixWithUncompressedSize())
     {
-      uncompressedSize = ReadUncompressedSizePrefix(compressed, compressedSize);
+      hasMaximumSize = true;
+      maximumSize = ReadUncompressedSizePrefix(compressed, compressedSize);
       source += sizeof(uint64_t);
       compressedSize -= sizeof(uint64_t);
     }
     else
     {
-      uncompressedSize = GuessUncompressedSize(compressed, compressedSize);
-    }
-
-    try
-    {
-      uncompressed.resize(static_cast<size_t>(uncompressedSize));
-    }
-    catch (...)
-    {
-      throw OrthancException(ErrorCode_NotEnoughMemory);
+      // TODO
     }
 
     z_stream stream;
     memset(&stream, 0, sizeof(stream));
 
-    char dummy = '\0';  // zlib does not like NULL output buffers (even if the uncompressed data is empty)
     stream.next_in = const_cast<Bytef*>(source);
-    stream.next_out = reinterpret_cast<Bytef*>(uncompressedSize == 0 ? &dummy : &uncompressed[0]);
-
     stream.avail_in = static_cast<uInt>(compressedSize);
-    stream.avail_out = static_cast<uInt>(uncompressedSize);
 
-    // Ensure no overflow (if the buffer is too large for the current archicture)
-    if (static_cast<size_t>(stream.avail_in) != compressedSize ||
-        static_cast<size_t>(stream.avail_out) != uncompressedSize)
+    // Ensure no overflow (if the buffer is too large for the current zlib archicture)
+    if (static_cast<size_t>(stream.avail_in) != compressedSize)
     {
       throw OrthancException(ErrorCode_NotEnoughMemory);
     }
 
-    // Initialize the compression engine
-    int error = inflateInit2(&stream, 
-                             MAX_WBITS + 16);  // this is a gzip input
+    ChunkedBuffer buffer;
+
+    {
+      GzipRaii raii(stream);
 
-    if (error != Z_OK)
-    {
-      // Cannot initialize zlib
-      uncompressed.clear();
-      throw OrthancException(ErrorCode_InternalError);
-    }
+      std::string chunk;
+      chunk.resize(10 * 1024 * 1024); // Read by chunks of 10MB
+
+      int ret = Z_OK;
 
-    // Uncompress the input buffer
-    error = inflate(&stream, Z_FINISH);
+      while (ret != Z_STREAM_END)
+      {
+        stream.next_out  = reinterpret_cast<Bytef*>(chunk.data());
+        stream.avail_out = static_cast<uInt>(chunk.size());
+
+        ret = inflate(&stream, Z_NO_FLUSH);
 
-    if (error != Z_STREAM_END)
-    {
-      inflateEnd(&stream);
-      uncompressed.clear();
+        switch (ret)
+        {
+        case Z_STREAM_END:
+        case Z_OK:
+          break; // Normal
 
-      switch (error)
-      {
+        case Z_NEED_DICT:
+        case Z_DATA_ERROR:
+        case Z_STREAM_ERROR:
+          throw OrthancException(ErrorCode_BadFileFormat);
+
         case Z_MEM_ERROR:
           throw OrthancException(ErrorCode_NotEnoughMemory);
-          
+
         case Z_BUF_ERROR:
-        case Z_NEED_DICT:
-          throw OrthancException(ErrorCode_BadFileFormat);
-          
+          // Not fatal: means no progress was possible this round.
+          // If avail_in is also 0 here, the input is truncated.
+          if (stream.avail_in == 0)
+          {
+            throw OrthancException(ErrorCode_BadFileFormat, "Truncated gzip input");
+          }
+          break;
+
         default:
-          throw OrthancException(ErrorCode_InternalError);
+          throw OrthancException(ErrorCode_InternalError); // Unknown error
+        }
+
+        const size_t produced = chunk.size() - stream.avail_out;
+
+        if (hasMaximumSize &&
+            buffer.GetNumBytes() + produced > maximumSize)
+        {
+          char s[32];
+          sprintf(s, "%0.1f", static_cast<float>(maximumSize) / (1024.0f * 1024.0f));
+          throw OrthancException(ErrorCode_BadFileFormat, "Uncompressed size exceeds limit (" + std::string(s) + "MB)");
+        }
+        else
+        {
+          // OK, add the chunk
+          buffer.AddChunk(&chunk[0], produced);
+        }
       }
     }
 
-    size_t size = stream.total_out;
-
-    if (inflateEnd(&stream) != Z_OK)
-    {
-      uncompressed.clear();
-      throw OrthancException(ErrorCode_InternalError);
-    }
-
-    if (size != uncompressedSize)
-    {
-      uncompressed.clear();
-
-      // The uncompressed size was not that properly guess, presumably
-      // because of a file size over 4GB. Should fallback to
-      // stream-based decompression.
-      throw OrthancException(ErrorCode_NotImplemented,
-                             "The uncompressed size of a gzip-encoded buffer was not properly guessed");
-    }
+    buffer.Flatten(uncompressed);
   }
 }
--- a/OrthancFramework/Sources/Compression/ZipReader.cpp	Sun Mar 22 11:26:21 2026 +0100
+++ b/OrthancFramework/Sources/Compression/ZipReader.cpp	Mon Mar 23 07:54:13 2026 +0100
@@ -425,8 +425,8 @@
             info.uncompressed_size > maximumUncompressedFileSize_)
         {
           char s[32];
-          sprintf(s, "%0.2f", static_cast<float>(info.uncompressed_size) / (1024.0f * 1024.0f));
-          throw OrthancException(ErrorCode_BadFileFormat, "Uncompressed size exceeds limit: " + std::string(s) + "MB");
+          sprintf(s, "%0.1f", static_cast<float>(maximumUncompressedFileSize_) / (1024.0f * 1024.0f));
+          throw OrthancException(ErrorCode_BadFileFormat, "Uncompressed size exceeds limit (" + std::string(s) + "MB)");
         }
       }