changeset 6658:d0a3f15d2793 machine-spirits

integration mainline->machine-spirits
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 20 Mar 2026 17:09:51 +0100
parents db756d9176cc (diff) ab12547ac3df (current diff)
children 81d6130cf13a
files
diffstat 10 files changed, 236 insertions(+), 74 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Fri Mar 20 17:05:49 2026 +0100
+++ b/NEWS	Fri Mar 20 17:09:51 2026 +0100
@@ -56,6 +56,13 @@
 * Save the jobs registry in DB only if it has changed.
   https://discourse.orthanc-server.org/t/frequent-idle-messages-between-postgres-and-orthanc/6406
 * New CMake option: "USE_SYSTEM_MINIZIP" to use the system-wide version of minizip
+* Security fixes courtesy of Machine Spirits UG:
+  - Fix possible out-of-bound access when calling /tools/create-dicom with a PAM file.
+  - Fix possible out-of-bound access when rows/columns DICOM tags exceed the maximum value for a US (65535).
+  - Fix possible out-of-bound access in PMSCT_RLE1 encoded images.
+  - Fix possible out-of-bound access in palette images.
+  - 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"
 * Upgraded dependencies for static builds:
   - boost 1.89.0
   - dcmtk 3.7.0
--- a/OrthancFramework/Sources/DicomFormat/DicomImageInformation.cpp	Fri Mar 20 17:05:49 2026 +0100
+++ b/OrthancFramework/Sources/DicomFormat/DicomImageInformation.cpp	Fri Mar 20 17:09:51 2026 +0100
@@ -42,6 +42,8 @@
 #include <stdio.h>
 #include <memory>
 
+static const uint64_t MAX_FRAME_SIZE = 4ul * 1024ul * 1024ul * 1024ul;  // defensive approach: set a reasonable max size for a frame
+
 namespace Orthanc
 {
   DicomImageInformation::DicomImageInformation(const DicomMap& values)
@@ -130,6 +132,11 @@
       values.GetValue(DICOM_TAG_COLUMNS).ParseFirstUnsignedInteger(width_); // in some US images, we've seen tag values of "800\0"; that's why we parse the 'first' value
       values.GetValue(DICOM_TAG_ROWS).ParseFirstUnsignedInteger(height_);
 
+      if (height_ > 65535 || width_ > 65535)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat, "Image width or height exceed DICOM VR US range (65535)");
+      }
+
       if (!values.ParseUnsignedInteger32(bitsAllocated_, DICOM_TAG_BITS_ALLOCATED))
       {
         throw OrthancException(ErrorCode_BadFileFormat);
@@ -454,13 +461,15 @@
 
   size_t DicomImageInformation::GetFrameSize() const
   {
+    uint64_t totalFrameSize;
+
     if (bitsStored_ == 1)
     {
       assert(GetWidth() % 8 == 0);
       
       if (GetChannelCount() == 1)
       {
-        return GetHeight() * GetWidth() / 8;
+        totalFrameSize = static_cast<uint64_t>(GetHeight()) * static_cast<uint64_t>(GetWidth()) / 8;
       }
       else
       {
@@ -470,10 +479,20 @@
     }
     else
     {
-      return (GetHeight() *
-              GetWidth() *
-              GetBytesPerValue() *
-              GetChannelCount());
+      totalFrameSize = (static_cast<uint64_t>(GetHeight()) *
+                        static_cast<uint64_t>(GetWidth()) *
+                        static_cast<uint64_t>(GetBytesPerValue()) *
+                        static_cast<uint64_t>(GetChannelCount()));
+    }
+
+    if (totalFrameSize > MAX_FRAME_SIZE ||
+        static_cast<uint64_t>(static_cast<size_t>(totalFrameSize)) != totalFrameSize)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat, "DICOM Frame size overflow");
+    }
+    else
+    {
+      return static_cast<size_t>(totalFrameSize);
     }
   }
 
--- a/OrthancFramework/Sources/DicomFormat/DicomIntegerPixelAccessor.cpp	Fri Mar 20 17:05:49 2026 +0100
+++ b/OrthancFramework/Sources/DicomFormat/DicomIntegerPixelAccessor.cpp	Fri Mar 20 17:09:51 2026 +0100
@@ -49,7 +49,7 @@
         information_.GetBitsStored() >= 32)
     {
       // Not available, as the accessor internally uses int32_t values
-      throw OrthancException(ErrorCode_NotImplemented);
+      throw OrthancException(ErrorCode_NotImplemented, "No support for BitsAllocated > 32 or BitsStored >= 32");
     }
 
     frame_ = 0;
@@ -57,7 +57,7 @@
 
     if (information_.GetNumberOfFrames() * frameOffset_ > size)
     {
-      throw OrthancException(ErrorCode_BadFileFormat);
+      throw OrthancException(ErrorCode_BadFileFormat, "Invalid size");
     }
 
     if (information_.IsSigned())
--- a/OrthancFramework/Sources/DicomFormat/DicomStreamReader.cpp	Fri Mar 20 17:05:49 2026 +0100
+++ b/OrthancFramework/Sources/DicomFormat/DicomStreamReader.cpp	Fri Mar 20 17:09:51 2026 +0100
@@ -204,6 +204,11 @@
       {
         uint16_t length = ReadUnsignedInteger16(p + pos + 6, true);
 
+        if (pos + 8 + length > block.size())
+        {
+          throw OrthancException(ErrorCode_BadFileFormat, "DICOM meta-header tag length exceeds available data");
+        }
+
         std::string value;
         value.assign(p + pos + 8, length);
         NormalizeValue(value, vr);
@@ -237,6 +242,11 @@
           
         uint32_t length = ReadUnsignedInteger32(p + pos + 8, true);
 
+        if (pos + 12 + static_cast<size_t>(length) > block.size())
+        {
+          throw OrthancException(ErrorCode_BadFileFormat, "DICOM meta-header tag length exceeds available data");
+        }
+
         if (tag.GetGroup() == 0x0002)
         {
           std::string value;
--- a/OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.cpp	Fri Mar 20 17:05:49 2026 +0100
+++ b/OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.cpp	Fri Mar 20 17:09:51 2026 +0100
@@ -192,6 +192,11 @@
     {
       if (inbuffer[i] == 0xa5)
       {
+        if (i + 2 >= length)
+        {
+          throw OrthancException(ErrorCode_BadFileFormat, "Truncated PMSCT_RLE1 escape sequence");
+        }
+
         temp.push_back(inbuffer[i+2]);
         for (uint8_t repeat = inbuffer[i + 1]; repeat != 0; repeat--)
         {
@@ -215,6 +220,11 @@
 
       if (temp[i] == 0x5a)
       {
+        if (i + 2 >= temp.size())
+        {
+          throw OrthancException(ErrorCode_BadFileFormat, "Truncated PMSCT_RLE1 delta sequence");
+        }
+
         uint16_t v1 = temp[i + 1];
         uint16_t v2 = temp[i + 2];
         value = (v2 << 8) + v1;
@@ -460,9 +470,12 @@
           throw OrthancException(ErrorCode_NotImplemented, std::string("Palette Color Lookup Table Descriptor invalid palette size: '") + r.c_str() + "'");
         }
 
-        if (pixelLength != target->GetWidth() * target->GetHeight())
+        const uint64_t expectedSize = (static_cast<uint64_t>(target->GetWidth()) *
+                                       static_cast<uint64_t>(target->GetHeight()));
+        
+        if (pixelLength != expectedSize)
         {
-          DcmElement *elem;
+          DcmElement *elem = NULL;
           Uint16 bitsAllocated = 0;
 
           if (!dataset.findAndGetUint16(DCM_BitsAllocated, bitsAllocated).good())
@@ -470,7 +483,8 @@
             throw OrthancException(ErrorCode_NotImplemented);  
           }
 
-          if (!dataset.findAndGetElement(DCM_PixelData, elem).good())
+          if (!dataset.findAndGetElement(DCM_PixelData, elem).good() ||
+              elem == NULL)
           {
             throw OrthancException(ErrorCode_NotImplemented);  
           }
@@ -478,9 +492,11 @@
           // In implicit VR files, pixelLength is expressed in words (OW) although pixels can actually be 8 bits
           // -> pixelLength is wrong by a factor of two and the image can still be decoded!
           // seen in some Philips ClearVue 650 images (using 8 bits LUT)
-          if (!(elem->getVR() == EVR_OW && bitsAllocated == 8 && (2*pixelLength == target->GetWidth() * target->GetHeight())))  
+          if (elem->getVR() != EVR_OW ||
+              bitsAllocated != 8 ||
+              2 * pixelLength != expectedSize)
           {
-            throw OrthancException(ErrorCode_NotImplemented);
+            throw OrthancException(ErrorCode_BadFileFormat, "Invalid size");
           }
         }
 
@@ -494,6 +510,11 @@
 
           for (unsigned int x = 0; x < width; x++)
           {
+            if (*source >= paletteSize)
+            {
+              throw OrthancException(ErrorCode_BadFileFormat, "Pixel value exceeds palette");
+            }
+
             p[0] = lutRed[*source] >> offsetBits;
             p[1] = lutGreen[*source] >> offsetBits;
             p[2] = lutBlue[*source] >> offsetBits;
--- a/OrthancFramework/Sources/HttpServer/HttpServer.cpp	Fri Mar 20 17:05:49 2026 +0100
+++ b/OrthancFramework/Sources/HttpServer/HttpServer.cpp	Fri Mar 20 17:09:51 2026 +0100
@@ -185,7 +185,8 @@
       PostDataStatus_Success,
       PostDataStatus_NoLength,
       PostDataStatus_Pending,
-      PostDataStatus_Failure
+      PostDataStatus_Failure,
+      PostDataStatus_RequestEntityTooLarge  // New in Orthanc 1.12.11
     };
   }
 
@@ -491,57 +492,19 @@
     reader.AddChunk(body);        
     reader.CloseStream();
   }
-  
 
-  static PostDataStatus ReadBodyWithContentLength(std::string& body,
-                                                  struct mg_connection *connection,
-                                                  const std::string& contentLength)
-  {
-    size_t length;
-    try
-    {
-      int64_t tmp = boost::lexical_cast<int64_t>(contentLength);
-      if (tmp < 0)
-      {
-        return PostDataStatus_NoLength;
-      }
-
-      length = static_cast<size_t>(tmp);
-    }
-    catch (boost::bad_lexical_cast&)
-    {
-      return PostDataStatus_NoLength;
-    }
 
-    body.resize(length);
-
-    size_t pos = 0;
-    while (length > 0)
-    {
-      int r = mg_read(connection, &body[pos], length);
-      if (r <= 0)
-      {
-        return PostDataStatus_Failure;
-      }
-
-      assert(static_cast<size_t>(r) <= length);
-      length -= r;
-      pos += r;
-    }
-
-    return PostDataStatus_Success;
-  }
-                                                  
-
-  static PostDataStatus ReadBodyWithoutContentLength(std::string& body,
-                                                     struct mg_connection *connection)
+  static PostDataStatus ReadBodyUsingFile(std::string& body,
+                                          struct mg_connection *connection,
+                                          size_t maxSize /* "0" means no limit */)
   {
     // Store the individual chunks in a temporary file, then read it
     // back into the memory buffer "body"
     FileBuffer buffer;
 
+    uint64_t readSoFar = 0;
     std::string tmp(1024 * 1024, 0);
-      
+
     for (;;)
     {
       int r = mg_read(connection, &tmp[0], tmp.size());
@@ -555,6 +518,15 @@
       }
       else
       {
+        readSoFar += r;
+
+        if (readSoFar > std::numeric_limits<size_t>::max() ||
+            (maxSize != 0 &&
+             readSoFar > maxSize))
+        {
+          return PostDataStatus_RequestEntityTooLarge;
+        }
+
         buffer.Append(tmp.c_str(), r);
       }
     }
@@ -563,6 +535,88 @@
 
     return PostDataStatus_Success;
   }
+
+
+  static PostDataStatus ReadBodyWithContentLength(std::string& body,
+                                                  struct mg_connection *connection,
+                                                  const std::string& contentLength)
+  {
+    static const size_t MAXIMUM_BODY_SIZE_IN_MEMORY = 10 * 1024 * 1024;  // 10MB
+
+    size_t length;
+    try
+    {
+      int64_t tmp = boost::lexical_cast<int64_t>(contentLength);
+      if (tmp < 0)
+      {
+        return PostDataStatus_NoLength;
+      }
+
+      length = static_cast<size_t>(tmp);
+      if (static_cast<int64_t>(length) != tmp)
+      {
+        return PostDataStatus_Failure;
+      }
+    }
+    catch (boost::bad_lexical_cast&)
+    {
+      return PostDataStatus_NoLength;
+    }
+
+    if (length < MAXIMUM_BODY_SIZE_IN_MEMORY)
+    {
+      /**
+       * Small POST bodies should land into RAM to avoid creating
+       * temporary files, which would result in bad performance.
+       **/
+      body.resize(length);
+
+      size_t pos = 0;
+      while (length > 0)
+      {
+        int r = mg_read(connection, &body[pos], length);
+        if (r <= 0)
+        {
+          return PostDataStatus_Failure;
+        }
+
+        assert(static_cast<size_t>(r) <= length);
+        length -= r;
+        pos += r;
+      }
+
+      return PostDataStatus_Success;
+    }
+    else
+    {
+      /**
+       * Deal with CWE-770 (Machine Spirits UG). If the client wants
+       * to send a large body, use a temporary file to prevent memory
+       * exhaustion by a malicious client that would set a large
+       * "Content-Length" without sending any actual data.
+       **/
+
+      PostDataStatus status = ReadBodyUsingFile(body, connection, length);
+
+      if (status == PostDataStatus_Success)
+      {
+        return (body.size() == length ?
+                PostDataStatus_Success :
+                PostDataStatus_Failure);
+      }
+      else
+      {
+        return status;
+      }
+    }
+  }
+
+
+  static PostDataStatus ReadBodyWithoutContentLength(std::string& body,
+                                                     struct mg_connection *connection)
+  {
+    return ReadBodyUsingFile(body, connection, 0 /* TODO - no bound */);
+  }
                                                   
 
   static PostDataStatus ReadBodyToString(std::string& body,
@@ -1492,6 +1546,10 @@
           output.SendStatus(HttpStatus_411_LengthRequired);
           return;
 
+        case PostDataStatus_RequestEntityTooLarge:  // New in Orthanc 1.12.11
+          output.SendStatus(HttpStatus_413_RequestEntityTooLarge);
+          return;
+
         case PostDataStatus_Failure:
           output.SendStatus(HttpStatus_400_BadRequest);
           return;
--- a/OrthancFramework/Sources/Images/ImageAccessor.cpp	Fri Mar 20 17:05:49 2026 +0100
+++ b/OrthancFramework/Sources/Images/ImageAccessor.cpp	Fri Mar 20 17:09:51 2026 +0100
@@ -165,7 +165,14 @@
   {
     if (buffer_ != NULL)
     {
-      return buffer_ + static_cast<size_t>(y) * static_cast<size_t>(pitch_);
+      if (y < height_)
+      {
+        return buffer_ + static_cast<size_t>(y) * static_cast<size_t>(pitch_);
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
     }
     else
     {
@@ -184,7 +191,14 @@
 
     if (buffer_ != NULL)
     {
-      return buffer_ + static_cast<size_t>(y) * static_cast<size_t>(pitch_);
+      if (y < height_)
+      {
+        return buffer_ + static_cast<size_t>(y) * static_cast<size_t>(pitch_);
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
     }
     else
     {
@@ -210,17 +224,20 @@
                                      unsigned int pitch,
                                      const void *buffer)
   {
+    const uint64_t size = static_cast<uint64_t>(height) * static_cast<uint64_t>(pitch);
+
+    if (static_cast<uint64_t>(GetBytesPerPixel() * width) > static_cast<uint64_t>(pitch) ||
+        static_cast<uint64_t>(static_cast<size_t>(size)) != size)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
     readOnly_ = true;
     format_ = format;
     width_ = width;
     height_ = height;
     pitch_ = pitch;
     buffer_ = reinterpret_cast<uint8_t*>(const_cast<void*>(buffer));
-
-    if (GetBytesPerPixel() * width_ > pitch_)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
   }
 
   void ImageAccessor::GetReadOnlyAccessor(ImageAccessor &target) const
@@ -235,17 +252,20 @@
                                      unsigned int pitch,
                                      void *buffer)
   {
+    const uint64_t size = static_cast<uint64_t>(height) * static_cast<uint64_t>(pitch);
+
+    if (static_cast<uint64_t>(GetBytesPerPixel() * width) > static_cast<uint64_t>(pitch) ||
+        static_cast<uint64_t>(static_cast<size_t>(size)) != size)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
     readOnly_ = false;
     format_ = format;
     width_ = width;
     height_ = height;
     pitch_ = pitch;
     buffer_ = reinterpret_cast<uint8_t*>(buffer);
-
-    if (GetBytesPerPixel() * width_ > pitch_)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
   }
 
 
--- a/OrthancFramework/Sources/Images/ImageBuffer.cpp	Fri Mar 20 17:05:49 2026 +0100
+++ b/OrthancFramework/Sources/Images/ImageBuffer.cpp	Fri Mar 20 17:09:51 2026 +0100
@@ -46,8 +46,16 @@
         }
       */
 
-      pitch_ = GetBytesPerPixel() * width_;
-      size_t size = static_cast<size_t>(pitch_) * static_cast<size_t>(height_);
+      const uint64_t tmpPitch = static_cast<uint64_t>(GetBytesPerPixel()) * static_cast<uint64_t>(width_);
+      const uint64_t size = tmpPitch * static_cast<uint64_t>(height_);
+
+      if (static_cast<uint64_t>(static_cast<unsigned int>(tmpPitch)) != tmpPitch ||
+          static_cast<uint64_t>(static_cast<size_t>(size)) != size)
+      {
+        throw OrthancException(ErrorCode_NotEnoughMemory);
+      }
+
+      pitch_ = static_cast<unsigned int>(tmpPitch);
 
       if (size == 0)
       {
@@ -55,7 +63,7 @@
       }
       else
       {
-        buffer_ = malloc(size);
+        buffer_ = malloc(static_cast<size_t>(size));
         if (buffer_ == NULL)
         {
           throw OrthancException(ErrorCode_NotEnoughMemory,
--- a/OrthancFramework/Sources/Images/PamReader.cpp	Fri Mar 20 17:05:49 2026 +0100
+++ b/OrthancFramework/Sources/Images/PamReader.cpp	Fri Mar 20 17:09:51 2026 +0100
@@ -38,6 +38,7 @@
 #include <boost/algorithm/string/find.hpp>
 #include <boost/lexical_cast.hpp>
 
+static const uint64_t MAX_PAM_IMAGE_BUFFER_SIZE = 4ul * 1024ul * 1024ul * 1024ul;  // defensive approach: set a reasonable max size for a PAM image
 
 namespace Orthanc
 {
@@ -181,11 +182,29 @@
     const unsigned int maxValue = LookupIntegerParameter(parameters, "MAXVAL");
     const std::string tupleType = LookupStringParameter(parameters, "TUPLTYPE");
 
+    if (width > 65535 || height > 65535 || channelCount > 4 || maxValue > 65535)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat, "PAM header values exceed reasonable limits");
+    }
+
     unsigned int bytesPerChannel;
     PixelFormat format;
     GetPixelFormat(format, bytesPerChannel, maxValue, channelCount, tupleType);
 
-    unsigned int pitch = width * channelCount * bytesPerChannel;
+    // unsigned int pitch = width * channelCount * bytesPerChannel;
+    uint64_t pitch = static_cast<uint64_t>(width) * channelCount * bytesPerChannel;
+
+    if (pitch > std::numeric_limits<unsigned int>::max())
+    {
+      throw OrthancException(ErrorCode_BadFileFormat, "PAM dimensions exceed limits");
+    }
+
+    uint64_t totalSize = pitch * height;
+    if (totalSize > MAX_PAM_IMAGE_BUFFER_SIZE ||
+        static_cast<uint64_t>(static_cast<size_t>(totalSize)) != totalSize)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat, "PAM image too large");
+    }
 
     if (content_.size() != header.size() + headerDelimiter.size() + pitch * height)
     {
@@ -196,7 +215,7 @@
 
     {
       intptr_t bufferAddr = reinterpret_cast<intptr_t>(&content_[offset]);
-      if((bufferAddr % 8) == 0)
+      if ((bufferAddr % 8) == 0)
         LOG(TRACE) << "PamReader::ParseContent() image address = " << bufferAddr;
       else
         LOG(TRACE) << "PamReader::ParseContent() image address = " << bufferAddr << " (not a multiple of 8!)";
--- a/OrthancServer/Sources/ServerContext.cpp	Fri Mar 20 17:05:49 2026 +0100
+++ b/OrthancServer/Sources/ServerContext.cpp	Fri Mar 20 17:09:51 2026 +0100
@@ -1950,7 +1950,7 @@
       }
       catch (OrthancException& e)
       {
-        LOG(INFO) << e.GetDetails();
+        LOG(INFO) << "Failed to decode a DICOM frame: " << e.GetDetails();
       }
     }