CVE-2026-10528
Description
A security flaw has been discovered in Orthanc DICOM Server up to 1.12.11. This issue affects the function DcmItem::read of the file OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp of the component DCMTK Parser. Performing a manipulation results in stack-based buffer overflow. Attacking locally is a requirement. The exploit has been released to the public and may be used for attacks. The patch is named bae99026ca97. To fix this issue, it is recommended to deploy a patch.
Affected products
1- Range: <=1.12.11
Patches
2847d50e83ae5Lower nesting level depth test for Windows.
1 file changed · +6 −3
dcmdata/tests/tnesting.cc+6 −3 modified@@ -172,14 +172,17 @@ OFTEST(dcmdata_nestingDepthLimit_customLimit) OFTEST(dcmdata_nestingDepthLimit_disabled) { /* Test that setMaxNestingDepth(-1) disables the limit entirely. - * Even 200 levels of nesting must succeed. */ + * Even 70 levels of nesting (above the default limit of 64) + * must succeed. We avoid going much higher because the + * recursive read() call chain can overflow the 1 MB default + * stack on Windows. */ DcmDataset srcDset, dstDset; - buildNestedDataset(200, srcDset); + buildNestedDataset(70, srcDset); OFCondition cond = serializeAndParse(srcDset, dstDset, -1); if (cond.bad()) { - OFCHECK_FAIL("Parsing 200 levels with maxNestingDepth=-1 (unlimited) should succeed, but got: " << cond.text()); + OFCHECK_FAIL("Parsing 70 levels with maxNestingDepth=-1 (unlimited) should succeed, but got: " << cond.text()); } }
885ff0f10372Add sequence nesting depth limit for parsing
24 files changed · +876 −16
config/docs/macros.txt+15 −0 modified@@ -109,6 +109,21 @@ DCMTK_LOG4CPLUS_AVOID_WIN32_FLS dcmtk::log4cplus::threadCleanup() should be called by the user code in order to clean-up oflog's thread local storage. +DCMTK_MAX_SEQUENCE_NESTING + Affected: dcmdata + Type of modification: Compile-time tunable + Explanation: Defines the default maximum permitted sequence nesting depth + during DICOM parsing. Deeply nested sequences can cause a stack overflow + by exhausting the call stack through unbounded recursion. When this macro + is not defined, the default limit of 64 nested sequence levels is used. + Real-world DICOM data rarely exceeds 5-10 nesting levels. The limit can + be changed at runtime per parse operation via + DcmInputStream::setMaxNestingDepth(), DcmItem::setMaxNestingDepth(), or + DcmFileFormat::setMaxNestingDepth(). Setting the runtime value to -1 + disables the check entirely. + Minimum value: 1. Values less than 1 cause a compile-time error. + Maximum value: 2147483647 (aligned to 32-bit signed integer runtime API). + DCMTK_MERGE_STDERR_TO_STDOUT Affected: dcmdata Type of modification: Activates feature
dcmdata/include/dcmtk/dcmdata/dcerror.h+3 −1 modified@@ -1,6 +1,6 @@ /* * - * Copyright (C) 1994-2025, OFFIS e.V. + * Copyright (C) 1994-2026, OFFIS e.V. * All rights reserved. See COPYRIGHT file for details. * * This software and supporting documentation were developed by @@ -200,6 +200,8 @@ extern DCMTK_DCMDATA_EXPORT const OFConditionConst EC_BulkDataURINotSupported; extern DCMTK_DCMDATA_EXPORT const OFConditionConst EC_UnsupportedURIType; /// Execution of command line failed extern DCMTK_DCMDATA_EXPORT const OFConditionConst EC_CommandLineFailed; +/// Maximum sequence nesting depth exceeded (stack overflow protection) +extern DCMTK_DCMDATA_EXPORT const OFConditionConst EC_NestingDepthLimitExceeded; ///@}
dcmdata/include/dcmtk/dcmdata/dcfilefo.h+29 −0 modified@@ -520,6 +520,35 @@ class DCMTK_DCMDATA_EXPORT DcmFileFormat /// implementation version name to write in the meta-header OFString ImplementationVersionName; + + /// maximum sequence nesting depth for parsing + /// (0 = compile-time default DCMTK_MAX_SEQUENCE_NESTING, -1 = unlimited) + Sint32 MaxNestingDepth; + + public: + + /** set the maximum permitted sequence nesting depth for parsing. + * Applied to the input stream in loadFile() and read(), and also + * forwarded to the contained dataset. + * - Value 0 (default): apply the compile-time default + * (DCMTK_MAX_SEQUENCE_NESTING, default is 64) + * - Value -1: disable the check (allow unlimited nesting) + * - Value > 0: use this value as the maximum permitted nesting depth + * @param maxDepth maximum nesting depth setting + */ + void setMaxNestingDepth(Sint32 maxDepth) + { + MaxNestingDepth = maxDepth; + DcmDataset* dset = getDataset(); + if (dset) + dset->setMaxNestingDepth(maxDepth); + } + + /** return the maximum permitted sequence nesting depth for parsing. + * @return maximum nesting depth setting + * (0 = compile-time default DCMTK_MAX_SEQUENCE_NESTING, -1 = unlimited) + */ + Sint32 getMaxNestingDepth() const { return MaxNestingDepth; } };
dcmdata/include/dcmtk/dcmdata/dcistrma.h+41 −1 modified@@ -1,6 +1,6 @@ /* * - * Copyright (C) 1994-2018, OFFIS e.V. + * Copyright (C) 1994-2026, OFFIS e.V. * All rights reserved. See COPYRIGHT file for details. * * This software and supporting documentation were developed by @@ -224,6 +224,40 @@ class DCMTK_DCMDATA_EXPORT DcmInputStream */ virtual DcmInputStreamFactory *newFactory() const = 0; + /** returns the current sequence nesting depth. + * This counter is used to prevent stack overflow from deeply nested + * DICOM sequences in malicious input files. + * @return current nesting depth + */ + Uint32 nestingDepth() const; + + /** increments the sequence nesting depth counter. + * @return the new nesting depth after incrementing + */ + Uint32 incrementNestingDepth(); + + /** decrements the sequence nesting depth counter. + * Does nothing if the counter is already zero. + */ + void decrementNestingDepth(); + + /** returns the maximum permitted sequence nesting depth for this stream. + * A value of 0 means the compile-time default (DCMTK_MAX_SEQUENCE_NESTING) applies. + * A value of -1 means the check is disabled (unlimited nesting). + * @return maximum nesting depth setting + */ + Sint32 maxNestingDepth() const; + + /** sets the maximum permitted sequence nesting depth for this stream. + * Must be called before parsing begins. + * - Value 0 (default): apply the compile-time default + * (DCMTK_MAX_SEQUENCE_NESTING, default is 64) + * - Value -1: disable the check (allow unlimited nesting) + * - Value > 0: use this value as the maximum permitted nesting depth + * @param maxDepth maximum nesting depth setting + */ + void setMaxNestingDepth(Sint32 maxDepth); + /** marks the current stream position for a later putback operation, * overwriting a possibly existing prior putback mark. * The DcmObject read methods rely on the possibility to putback @@ -272,6 +306,12 @@ class DCMTK_DCMDATA_EXPORT DcmInputStream /// putback marker offile_off_t mark_; + + /// current sequence nesting depth (for stack overflow protection) + Uint32 nestingDepth_; + + /// maximum permitted sequence nesting depth (0 = default 64, -1 = unlimited) + Sint32 maxNestingDepth_; };
dcmdata/include/dcmtk/dcmdata/dcitem.h+24 −1 modified@@ -1,6 +1,6 @@ /* * - * Copyright (C) 1994-2025, OFFIS e.V. + * Copyright (C) 1994-2026, OFFIS e.V. * All rights reserved. See COPYRIGHT file for details. * * This software and supporting documentation were developed by @@ -1573,6 +1573,29 @@ class DCMTK_DCMDATA_EXPORT DcmItem /// cache for private creator tags and identifiers DcmPrivateTagCache privateCreatorCache; + + /// maximum sequence nesting depth for parsing + /// (0 = compile-time default DCMTK_MAX_SEQUENCE_NESTING, -1 = unlimited) + Sint32 maxNestingDepth; + + public: + + /** set the maximum permitted sequence nesting depth for parsing. + * This limit is applied to the input stream before parsing in + * loadFile() and read(). + * - Value 0 (default): apply the compile-time default + * (DCMTK_MAX_SEQUENCE_NESTING, default is 64) + * - Value -1: disable the check (allow unlimited nesting) + * - Value > 0: use this value as the maximum permitted nesting depth + * @param maxDepth maximum nesting depth setting + */ + void setMaxNestingDepth(Sint32 maxDepth) { maxNestingDepth = maxDepth; } + + /** return the maximum permitted sequence nesting depth for parsing. + * @return maximum nesting depth setting + * (0 = compile-time default DCMTK_MAX_SEQUENCE_NESTING, -1 = unlimited) + */ + Sint32 getMaxNestingDepth() const { return maxNestingDepth; } }; /** Checks whether left hand side item is smaller than right hand side
dcmdata/include/dcmtk/dcmdata/dcobject.h+13 −1 modified@@ -1,6 +1,6 @@ /* * - * Copyright (C) 1994-2024, OFFIS e.V. + * Copyright (C) 1994-2026, OFFIS e.V. * All rights reserved. See COPYRIGHT file for details. * * This software and supporting documentation were developed by @@ -50,6 +50,18 @@ class DcmSpecificCharacterSet; // Undefined Length Identifier now defined in dctypes.h +// Default maximum sequence nesting depth (can be overridden at compile time). +// Must be in the range [1, 2147483647]. +#ifndef DCMTK_MAX_SEQUENCE_NESTING +#define DCMTK_MAX_SEQUENCE_NESTING 64 +#endif +#if DCMTK_MAX_SEQUENCE_NESTING < 1 +#error "DCMTK_MAX_SEQUENCE_NESTING must be >= 1" +#endif +#if DCMTK_MAX_SEQUENCE_NESTING > 2147483647 +#error "DCMTK_MAX_SEQUENCE_NESTING must be <= 2147483647" +#endif + // Maximum number of read bytes for a Value Element const Uint32 DCM_MaxReadLength = 4096;
dcmdata/libsrc/dcdatset.cc+6 −0 modified@@ -647,6 +647,9 @@ OFCondition DcmDataset::loadFileUntilTag(const OFFilename &fileName, { /* use stdin stream */ DcmStdinStream inStream; + /* apply configured nesting depth limit */ + if (getMaxNestingDepth() > 0) + inStream.setMaxNestingDepth(getMaxNestingDepth()); /* clear this object */ l_error = clear(); @@ -670,6 +673,9 @@ OFCondition DcmDataset::loadFileUntilTag(const OFFilename &fileName, } else { /* open file for input */ DcmInputFileStream fileStream(fileName); + /* apply configured nesting depth limit */ + if (getMaxNestingDepth() > 0) + fileStream.setMaxNestingDepth(getMaxNestingDepth()); /* check stream status */ l_error = fileStream.status();
dcmdata/libsrc/dcerror.cc+1 −0 modified@@ -91,6 +91,7 @@ makeOFConditionConst(EC_InvalidJSONContent, OFM_dcmdata, 65, OF_err makeOFConditionConst(EC_BulkDataURINotSupported, OFM_dcmdata, 66, OF_error, "BulkDataURI not yet supported" ); makeOFConditionConst(EC_UnsupportedURIType, OFM_dcmdata, 67, OF_error, "Unsupported URI type" ); makeOFConditionConst(EC_CommandLineFailed, OFM_dcmdata, 68, OF_error, "Execution of command line failed" ); +makeOFConditionConst(EC_NestingDepthLimitExceeded, OFM_dcmdata, 69, OF_error, "Maximum sequence nesting depth exceeded" ); const unsigned short EC_CODE_CannotSelectCharacterSet = 35; const unsigned short EC_CODE_CannotConvertCharacterSet = 36;
dcmdata/libsrc/dcfilefo.cc+13 −3 modified@@ -54,7 +54,8 @@ DcmFileFormat::DcmFileFormat() : DcmSequenceOfItems(DCM_InternalUseTag), FileReadMode(ERM_autoDetect), ImplementationClassUID(OFFIS_IMPLEMENTATION_CLASS_UID), - ImplementationVersionName(OFFIS_DTK_IMPLEMENTATION_VERSION_NAME) + ImplementationVersionName(OFFIS_DTK_IMPLEMENTATION_VERSION_NAME), + MaxNestingDepth(0) { DcmMetaInfo *MetaInfo = new DcmMetaInfo(); DcmSequenceOfItems::itemList->insert(MetaInfo); @@ -71,7 +72,8 @@ DcmFileFormat::DcmFileFormat(DcmDataset *dataset, : DcmSequenceOfItems(DCM_InternalUseTag), FileReadMode(ERM_autoDetect), ImplementationClassUID(OFFIS_IMPLEMENTATION_CLASS_UID), - ImplementationVersionName(OFFIS_DTK_IMPLEMENTATION_VERSION_NAME) + ImplementationVersionName(OFFIS_DTK_IMPLEMENTATION_VERSION_NAME), + MaxNestingDepth(0) { DcmMetaInfo *MetaInfo = new DcmMetaInfo(); if (DcmSequenceOfItems::itemList->insert(MetaInfo)) @@ -105,7 +107,8 @@ DcmFileFormat::DcmFileFormat(const DcmFileFormat &old) : DcmSequenceOfItems(old), FileReadMode(old.FileReadMode), ImplementationClassUID(old.ImplementationClassUID), - ImplementationVersionName(old.ImplementationVersionName) + ImplementationVersionName(old.ImplementationVersionName), + MaxNestingDepth(old.MaxNestingDepth) { } @@ -134,6 +137,7 @@ DcmFileFormat &DcmFileFormat::operator=(const DcmFileFormat &obj) FileReadMode = obj.FileReadMode; ImplementationClassUID = obj.ImplementationClassUID; ImplementationVersionName = obj.ImplementationVersionName; + MaxNestingDepth = obj.MaxNestingDepth; } return *this; } @@ -962,6 +966,9 @@ OFCondition DcmFileFormat::loadFileUntilTag( { /* use stdin stream */ DcmStdinStream inStream; + /* apply configured nesting depth limit */ + if (MaxNestingDepth != 0) + inStream.setMaxNestingDepth(MaxNestingDepth); /* clear this object */ l_error = clear(); @@ -992,6 +999,9 @@ OFCondition DcmFileFormat::loadFileUntilTag( } else { /* open file for output */ DcmInputFileStream fileStream(fileName); + /* apply configured nesting depth limit */ + if (MaxNestingDepth > 0) + fileStream.setMaxNestingDepth(MaxNestingDepth); /* check stream status */ l_error = fileStream.status();
dcmdata/libsrc/dcistrma.cc+29 −1 modified@@ -1,6 +1,6 @@ /* * - * Copyright (C) 1994-2010, OFFIS e.V. + * Copyright (C) 1994-2026, OFFIS e.V. * All rights reserved. See COPYRIGHT file for details. * * This software and supporting documentation were developed by @@ -29,6 +29,8 @@ DcmInputStream::DcmInputStream(DcmProducer *initial) , compressionFilter_(NULL) , tell_(0) , mark_(0) +, nestingDepth_(0) +, maxNestingDepth_(0) { } @@ -94,6 +96,32 @@ const DcmProducer *DcmInputStream::currentProducer() const return current_; } +Uint32 DcmInputStream::nestingDepth() const +{ + return nestingDepth_; +} + +Uint32 DcmInputStream::incrementNestingDepth() +{ + return ++nestingDepth_; +} + +void DcmInputStream::decrementNestingDepth() +{ + if (nestingDepth_ > 0) + --nestingDepth_; +} + +Sint32 DcmInputStream::maxNestingDepth() const +{ + return maxNestingDepth_; +} + +void DcmInputStream::setMaxNestingDepth(Sint32 maxDepth) +{ + maxNestingDepth_ = maxDepth; +} + OFCondition DcmInputStream::installCompressionFilter(E_StreamCompression filterType) { OFCondition result = EC_Normal;
dcmdata/libsrc/dcitem.cc+11 −3 modified@@ -83,7 +83,8 @@ DcmItem::DcmItem() elementList(NULL), lastElementComplete(OFTrue), fStartPosition(0), - privateCreatorCache() + privateCreatorCache(), + maxNestingDepth(0) { elementList = new DcmList; } @@ -95,7 +96,8 @@ DcmItem::DcmItem(const DcmTag &tag, elementList(NULL), lastElementComplete(OFTrue), fStartPosition(0), - privateCreatorCache() + privateCreatorCache(), + maxNestingDepth(0) { elementList = new DcmList; } @@ -106,7 +108,8 @@ DcmItem::DcmItem(const DcmItem &old) elementList(new DcmList), lastElementComplete(old.lastElementComplete), fStartPosition(old.fStartPosition), - privateCreatorCache() + privateCreatorCache(), + maxNestingDepth(old.maxNestingDepth) { if (!old.elementList->empty()) { @@ -138,6 +141,7 @@ DcmItem& DcmItem::operator=(const DcmItem& obj) // copy DcmItem's member variables lastElementComplete = obj.lastElementComplete; fStartPosition = obj.fStartPosition; + maxNestingDepth = obj.maxNestingDepth; if (!obj.elementList->empty()) { elementList->seek(ELP_first); @@ -1398,6 +1402,10 @@ OFCondition DcmItem::readUntilTag(DcmInputStream & inStream, return errorFlag; } + /* apply configured nesting depth limit to the input stream (0 = use default, skip override) */ + if (getMaxNestingDepth() != 0) + inStream.setMaxNestingDepth(getMaxNestingDepth()); + /* figure out if the stream reported an error */ errorFlag = inStream.status(); /* if the stream reported an error or if it is the end of the */
dcmdata/libsrc/dcsequen.cc+19 −1 modified@@ -1,6 +1,6 @@ /* * - * Copyright (C) 1994-2023, OFFIS e.V. + * Copyright (C) 1994-2026, OFFIS e.V. * All rights reserved. See COPYRIGHT file for details. * * This software and supporting documentation were developed by @@ -710,6 +710,23 @@ OFCondition DcmSequenceOfItems::read(DcmInputStream &inStream, errorFlag = EC_IllegalCall; else { + const Uint32 depth = inStream.incrementNestingDepth(); + const Sint32 maxDepthSetting = inStream.maxNestingDepth(); + /* -1 = unlimited; 0 = use built-in default (DCMTK_MAX_SEQUENCE_NESTING); > 0 = custom limit */ + const Uint32 effectiveMax = (maxDepthSetting == 0) ? DCMTK_MAX_SEQUENCE_NESTING + : (maxDepthSetting < 0) ? 0 /* unlimited */ + : OFstatic_cast(Uint32, maxDepthSetting); + if (effectiveMax > 0 && depth > effectiveMax) + { + DCMDATA_ERROR("DcmSequenceOfItems: Maximum nesting depth (" << effectiveMax + << ") exceeded while parsing sequence " << getTagName() << " " << getTag()); + inStream.decrementNestingDepth(); + errorFlag = EC_NestingDepthLimitExceeded; + // dump information if required + DCMDATA_TRACE("DcmSequenceOfItems::read() returns error = " << errorFlag.text()); + return errorFlag; + } + errorFlag = inStream.status(); if (errorFlag.good() && inStream.eos()) @@ -779,6 +796,7 @@ OFCondition DcmSequenceOfItems::read(DcmInputStream &inStream, errorFlag = EC_Normal; if (errorFlag.good()) setTransferState(ERW_ready); // sequence is complete + inStream.decrementNestingDepth(); } // dump information if required DCMDATA_TRACE("DcmSequenceOfItems::read() returns error = " << errorFlag.text());
dcmdata/tests/CMakeLists.txt+1 −0 modified@@ -11,6 +11,7 @@ DCMTK_ADD_TEST_EXECUTABLE(dcmdata_tests ti2dbmp.cc titem.cc tmatch.cc + tnesting.cc tnewdcme.cc tparent.cc tparser.cc
dcmdata/tests/Makefile.dep+67 −0 modified@@ -712,6 +712,73 @@ tmatch.o: tmatch.cc ../../config/include/dcmtk/config/osconfig.h \ ../include/dcmtk/dcmdata/dcvr.h \ ../../ofstd/include/dcmtk/ofstd/ofglobal.h \ ../include/dcmtk/dcmdata/dcmatch.h +tnesting.o: tnesting.cc ../../config/include/dcmtk/config/osconfig.h \ + ../../ofstd/include/dcmtk/ofstd/oftest.h \ + ../../ofstd/include/dcmtk/ofstd/ofconapp.h \ + ../../ofstd/include/dcmtk/ofstd/oftypes.h \ + ../../ofstd/include/dcmtk/ofstd/ofdefine.h \ + ../../ofstd/include/dcmtk/ofstd/ofcast.h \ + ../../ofstd/include/dcmtk/ofstd/ofexport.h \ + ../../ofstd/include/dcmtk/ofstd/ofstdinc.h \ + ../../ofstd/include/dcmtk/ofstd/ofcmdln.h \ + ../../ofstd/include/dcmtk/ofstd/ofexbl.h \ + ../../ofstd/include/dcmtk/ofstd/oftraits.h \ + ../../ofstd/include/dcmtk/ofstd/oflist.h \ + ../../ofstd/include/dcmtk/ofstd/ofstring.h \ + ../../ofstd/include/dcmtk/ofstd/ofstream.h \ + ../../ofstd/include/dcmtk/ofstd/ofconsol.h \ + ../../ofstd/include/dcmtk/ofstd/ofthread.h \ + ../../ofstd/include/dcmtk/ofstd/offile.h \ + ../../ofstd/include/dcmtk/ofstd/ofstd.h \ + ../../ofstd/include/dcmtk/ofstd/ofcond.h \ + ../../ofstd/include/dcmtk/ofstd/ofdiag.h \ + ../../ofstd/include/dcmtk/ofstd/diag/push.def \ + ../../ofstd/include/dcmtk/ofstd/diag/useafree.def \ + ../../ofstd/include/dcmtk/ofstd/diag/pop.def \ + ../../ofstd/include/dcmtk/ofstd/oflimits.h \ + ../../ofstd/include/dcmtk/ofstd/oferror.h \ + ../../ofstd/include/dcmtk/ofstd/ofexit.h \ + ../include/dcmtk/dcmdata/dcuid.h ../include/dcmtk/dcmdata/dcdefine.h \ + ../../oflog/include/dcmtk/oflog/oflog.h \ + ../../oflog/include/dcmtk/oflog/logger.h \ + ../../oflog/include/dcmtk/oflog/config.h \ + ../../oflog/include/dcmtk/oflog/config/defines.h \ + ../../oflog/include/dcmtk/oflog/helpers/threadcf.h \ + ../../oflog/include/dcmtk/oflog/loglevel.h \ + ../../ofstd/include/dcmtk/ofstd/ofvector.h \ + ../../oflog/include/dcmtk/oflog/tstring.h \ + ../../oflog/include/dcmtk/oflog/tchar.h \ + ../../oflog/include/dcmtk/oflog/spi/apndatch.h \ + ../../oflog/include/dcmtk/oflog/appender.h \ + ../../ofstd/include/dcmtk/ofstd/ofmem.h \ + ../../ofstd/include/dcmtk/ofstd/ofutil.h \ + ../../ofstd/include/dcmtk/ofstd/variadic/tuplefwd.h \ + ../../oflog/include/dcmtk/oflog/layout.h \ + ../../oflog/include/dcmtk/oflog/streams.h \ + ../../oflog/include/dcmtk/oflog/helpers/pointer.h \ + ../../oflog/include/dcmtk/oflog/thread/syncprim.h \ + ../../oflog/include/dcmtk/oflog/spi/filter.h \ + ../../oflog/include/dcmtk/oflog/helpers/lockfile.h \ + ../../oflog/include/dcmtk/oflog/spi/logfact.h \ + ../../oflog/include/dcmtk/oflog/logmacro.h \ + ../../oflog/include/dcmtk/oflog/helpers/snprintf.h \ + ../../oflog/include/dcmtk/oflog/tracelog.h \ + ../../ofstd/include/dcmtk/ofstd/oftempf.h \ + ../include/dcmtk/dcmdata/dcdatset.h ../include/dcmtk/dcmdata/dcitem.h \ + ../include/dcmtk/dcmdata/dctypes.h ../include/dcmtk/dcmdata/dcobject.h \ + ../../ofstd/include/dcmtk/ofstd/ofglobal.h \ + ../include/dcmtk/dcmdata/dcerror.h ../include/dcmtk/dcmdata/dcxfer.h \ + ../include/dcmtk/dcmdata/dcvr.h \ + ../../ofstd/include/dcmtk/ofstd/ofdeprec.h \ + ../include/dcmtk/dcmdata/dctag.h ../include/dcmtk/dcmdata/dctagkey.h \ + ../../ofstd/include/dcmtk/ofstd/diag/ignrattr.def \ + ../include/dcmtk/dcmdata/dcstack.h ../include/dcmtk/dcmdata/dclist.h \ + ../include/dcmtk/dcmdata/dcpcache.h ../include/dcmtk/dcmdata/dcfilefo.h \ + ../include/dcmtk/dcmdata/dcsequen.h ../include/dcmtk/dcmdata/dcelem.h \ + ../include/dcmtk/dcmdata/dcistrmb.h ../include/dcmtk/dcmdata/dcistrma.h \ + ../include/dcmtk/dcmdata/dcostrmb.h ../include/dcmtk/dcmdata/dcostrma.h \ + ../include/dcmtk/dcmdata/dcdeftag.h ../include/dcmtk/dcmdata/dcwcache.h \ + ../include/dcmtk/dcmdata/dcfcache.h tnewdcme.o: tnewdcme.cc ../../config/include/dcmtk/config/osconfig.h \ ../../ofstd/include/dcmtk/ofstd/oftest.h \ ../../ofstd/include/dcmtk/ofstd/ofconapp.h \
dcmdata/tests/Makefile.in+1 −1 modified@@ -25,7 +25,7 @@ LIBDCMXML = -ldcmxml objs = tests.o tpread.o ti2dbmp.o tchval.o tpath.o tvrdatim.o telemlen.o tparser.o \ tdict.o tvrds.o tvrfd.o tvrui.o tvrol.o tvrov.o tvrsv.o tvruv.o tstrval.o \ - tspchrs.o tvrpn.o tparent.o tfilter.o tvrcomp.o tmatch.o tnewdcme.o \ + tspchrs.o tvrpn.o tparent.o tfilter.o tvrcomp.o tmatch.o tnesting.o tnewdcme.o \ tgenuid.o tsequen.o titem.o ttag.o txfer.o tbytestr.o tfrmsiz.o progs = tests
dcmdata/tests/tests.cc+8 −1 modified@@ -1,6 +1,6 @@ /* * - * Copyright (C) 2011-2025 OFFIS e.V. + * Copyright (C) 2011-2026 OFFIS e.V. * All rights reserved. See COPYRIGHT file for details. * * This software and supporting documentation were developed by @@ -127,5 +127,12 @@ OFTEST_REGISTER(dcmdata_xferLookup_3); OFTEST_REGISTER(dcmdata_xferLookup_4); OFTEST_REGISTER(dcmdata_putOFStringAtPos); OFTEST_REGISTER(dcmdata_uncompressedFrameSize); +OFTEST_REGISTER(dcmdata_nestingDepthLimit_exceeded); +OFTEST_REGISTER(dcmdata_nestingDepthLimit_atLimit); +OFTEST_REGISTER(dcmdata_nestingDepthLimit_wellBelow); +OFTEST_REGISTER(dcmdata_nestingDepthLimit_customLimit); +OFTEST_REGISTER(dcmdata_nestingDepthLimit_disabled); +OFTEST_REGISTER(dcmdata_nestingDepthLimit_datasetAPI); +OFTEST_REGISTER(dcmdata_nestingDepthLimit_fileFormatAPI); OFTEST_MAIN("dcmdata")
dcmdata/tests/tnesting.cc+266 −0 added@@ -0,0 +1,266 @@ +/* + * + * Copyright (C) 2026, OFFIS e.V. + * All rights reserved. See COPYRIGHT file for details. + * + * This software and supporting documentation were developed by + * + * OFFIS e.V. + * R&D Division Health + * Escherweg 2 + * D-26121 Oldenburg, Germany + * + * + * Module: dcmdata + * + * Author: Michael Onken + * + * Purpose: test program for sequence nesting depth limit + * + */ + + +#include "dcmtk/config/osconfig.h" /* make sure OS specific configuration is included first */ + +#include "dcmtk/ofstd/oftest.h" +#include "dcmtk/ofstd/oftempf.h" +#include "dcmtk/dcmdata/dcdatset.h" +#include "dcmtk/dcmdata/dcfilefo.h" +#include "dcmtk/dcmdata/dcsequen.h" +#include "dcmtk/dcmdata/dcistrmb.h" +#include "dcmtk/dcmdata/dcostrmb.h" +#include "dcmtk/dcmdata/dcerror.h" +#include "dcmtk/dcmdata/dcdeftag.h" +#include "dcmtk/dcmdata/dcwcache.h" + + +/** Helper: build a DcmDataset containing a chain of nested sequences to the + * specified depth using the high-level dcmdata API. Each nesting level + * consists of a DcmSequenceOfItems with one DcmItem, and the innermost + * item is empty. + * @param depth the number of nested sequence levels to generate + * @param dset output dataset (cleared before use) + */ +static void buildNestedDataset(Uint32 depth, DcmDataset& dset) +{ + dset.clear(); + /* start with the dataset as the outermost item */ + DcmItem* currentItem = &dset; + for (Uint32 i = 0; i < depth; ++i) + { + DcmSequenceOfItems* seq = new DcmSequenceOfItems(DCM_ReferencedSeriesSequence); + currentItem->insert(seq); + DcmItem* item = new DcmItem(); + seq->insert(item); + currentItem = item; + } +} + + +/** Helper: serialize a dataset to a byte buffer and parse it back using a + * fresh DcmInputStream, with an optional custom nesting depth limit. + * @param srcDset the dataset to serialize + * @param dstDset output dataset to receive the parsed result + * @param maxNestingDepth nesting depth setting passed to setMaxNestingDepth(): + * 0 (default) = compile-time default (DCMTK_MAX_SEQUENCE_NESTING), -1 = unlimited, > 0 = custom limit + * @return the OFCondition returned by DcmDataset::read() + */ +static OFCondition serializeAndParse(DcmDataset& srcDset, DcmDataset& dstDset, + Sint32 maxNestingDepth = 0) +{ + /* write the dataset to a memory buffer */ + const size_t bufLen = 1024 * 1024; + Uint8* buf = new Uint8[bufLen]; + + DcmOutputBufferStream outStream(buf, bufLen); + srcDset.transferInit(); + DcmWriteCache wcache; + OFCondition cond = srcDset.write(outStream, EXS_LittleEndianExplicit, EET_UndefinedLength, &wcache); + srcDset.transferEnd(); + if (cond.bad()) + { + delete[] buf; + return cond; + } + offile_off_t bytesWritten = 0; + void* writtenBuf = NULL; + outStream.flushBuffer(writtenBuf, bytesWritten); + + /* parse it back with nesting depth limit */ + DcmInputBufferStream inStream; + inStream.setBuffer(buf, bytesWritten); + inStream.setEos(); + inStream.setMaxNestingDepth(maxNestingDepth); + + dstDset.clear(); + dstDset.transferInit(); + cond = dstDset.read(inStream, EXS_LittleEndianExplicit); + dstDset.transferEnd(); + + delete[] buf; + return cond; +} + + +OFTEST(dcmdata_nestingDepthLimit_exceeded) +{ + /* One level beyond the default limit (DCMTK_MAX_SEQUENCE_NESTING) must be rejected. */ + DcmDataset srcDset, dstDset; + buildNestedDataset(DCMTK_MAX_SEQUENCE_NESTING + 1, srcDset); + + OFCondition cond = serializeAndParse(srcDset, dstDset); + if (cond != EC_NestingDepthLimitExceeded) + { + OFCHECK_FAIL("Expected EC_NestingDepthLimitExceeded for " << (DCMTK_MAX_SEQUENCE_NESTING + 1) + << " levels of nesting, but got: " << cond.text()); + } +} + + +OFTEST(dcmdata_nestingDepthLimit_atLimit) +{ + /* Exactly DCMTK_MAX_SEQUENCE_NESTING levels must still pass. */ + DcmDataset srcDset, dstDset; + buildNestedDataset(DCMTK_MAX_SEQUENCE_NESTING, srcDset); + + OFCondition cond = serializeAndParse(srcDset, dstDset); + if (cond.bad()) + { + OFCHECK_FAIL("Parsing " << DCMTK_MAX_SEQUENCE_NESTING + << " levels of nesting should succeed, but got: " << cond.text()); + } +} + + +OFTEST(dcmdata_nestingDepthLimit_wellBelow) +{ + /* 5 levels of nesting (typical real-world depth) must parse fine. */ + DcmDataset srcDset, dstDset; + buildNestedDataset(5, srcDset); + + OFCondition cond = serializeAndParse(srcDset, dstDset); + if (cond.bad()) + { + OFCHECK_FAIL("Parsing 5 levels of nesting should succeed, but got: " << cond.text()); + } +} + + +OFTEST(dcmdata_nestingDepthLimit_customLimit) +{ + /* Test setMaxNestingDepth(): set the limit to 10. + * 10 levels must succeed, 11 levels must fail. */ + DcmDataset srcDset, dstDset; + OFCondition cond; + + buildNestedDataset(10, srcDset); + cond = serializeAndParse(srcDset, dstDset, 10); + if (cond.bad()) + { + OFCHECK_FAIL("Parsing 10 levels with maxNestingDepth=10 should succeed, but got: " << cond.text()); + } + + buildNestedDataset(11, srcDset); + cond = serializeAndParse(srcDset, dstDset, 10); + if (cond != EC_NestingDepthLimitExceeded) + { + OFCHECK_FAIL("Expected EC_NestingDepthLimitExceeded for 11 levels with maxNestingDepth=10, but got: " << cond.text()); + } +} + + +OFTEST(dcmdata_nestingDepthLimit_disabled) +{ + /* Test that setMaxNestingDepth(-1) disables the limit entirely. + * Even 200 levels of nesting must succeed. */ + DcmDataset srcDset, dstDset; + buildNestedDataset(200, srcDset); + + OFCondition cond = serializeAndParse(srcDset, dstDset, -1); + if (cond.bad()) + { + OFCHECK_FAIL("Parsing 200 levels with maxNestingDepth=-1 (unlimited) should succeed, but got: " << cond.text()); + } +} + + +/** Helper: create a temporary file path. + * @param tmpFile receives the temporary file path + * @return EC_Normal if successful + */ +static OFCondition makeTempFile(OFString& tmpFile) +{ + return OFTempFile::createFile(tmpFile, NULL /* fd_out */, O_RDWR, + "" /* dir */, "" /* prefix */, ".dcm" /* postfix */); +} + + +OFTEST(dcmdata_nestingDepthLimit_datasetAPI) +{ + /* Test DcmDataset::setMaxNestingDepth() with loadFile(). */ + DcmDataset srcDset; + OFString tmpFile; + OFCondition cond; + + /* save as raw dataset (no meta header) */ + buildNestedDataset(11, srcDset); + cond = makeTempFile(tmpFile); + OFCHECK(cond.good()); + cond = srcDset.saveFile(tmpFile, EXS_LittleEndianExplicit); + OFCHECK(cond.good()); + + /* default limit (DCMTK_MAX_SEQUENCE_NESTING): 11 levels should succeed */ + DcmDataset dset1; + cond = dset1.loadFile(tmpFile, EXS_LittleEndianExplicit); + if (cond.bad()) + { + OFCHECK_FAIL("DcmDataset::loadFile() with 11 levels should succeed with default limit, but got: " << cond.text()); + } + + /* custom limit 10: 11 levels should fail */ + DcmDataset dset2; + dset2.setMaxNestingDepth(10); + cond = dset2.loadFile(tmpFile, EXS_LittleEndianExplicit); + if (cond != EC_NestingDepthLimitExceeded) + { + OFCHECK_FAIL("DcmDataset::loadFile() with 11 levels and maxNestingDepth=10 should fail, but got: " << cond.text()); + } + + OFStandard::deleteFile(tmpFile); +} + + +OFTEST(dcmdata_nestingDepthLimit_fileFormatAPI) +{ + /* Test DcmFileFormat::setMaxNestingDepth() with loadFile(). */ + DcmDataset srcDset; + OFString tmpFile; + OFCondition cond; + + /* save as DcmFileFormat (with meta header) */ + buildNestedDataset(11, srcDset); + cond = makeTempFile(tmpFile); + OFCHECK(cond.good()); + DcmFileFormat srcFF(&srcDset); + cond = srcFF.saveFile(tmpFile, EXS_LittleEndianExplicit); + OFCHECK(cond.good()); + + /* default limit (DCMTK_MAX_SEQUENCE_NESTING): 11 levels should succeed */ + DcmFileFormat ff1; + cond = ff1.loadFile(tmpFile); + if (cond.bad()) + { + OFCHECK_FAIL("DcmFileFormat::loadFile() with 11 levels should succeed with default limit, but got: " << cond.text()); + } + + /* custom limit 10: 11 levels should fail */ + DcmFileFormat ff2; + ff2.setMaxNestingDepth(10); + cond = ff2.loadFile(tmpFile); + if (cond != EC_NestingDepthLimitExceeded) + { + OFCHECK_FAIL("DcmFileFormat::loadFile() with 11 levels and maxNestingDepth=10 should fail, but got: " << cond.text()); + } + + OFStandard::deleteFile(tmpFile); +}
dcmnet/include/dcmtk/dcmnet/scp.h+24 −0 modified@@ -360,6 +360,17 @@ class DCMTK_DCMNET_EXPORT DcmSCP */ void setAlwaysAcceptDefaultRole(const OFBool enabled); + /** Set the maximum permitted sequence nesting depth for parsing + * datasets received over the network. This limit is applied to + * each dataset received via receiveDIMSEDataset(). + * - Value 0 (default): apply the compile-time default + * (DCMTK_MAX_SEQUENCE_NESTING) + * - Value -1: disable the check (allow unlimited nesting) + * - Value > 0: use this value as the maximum nesting depth + * @param maxDepth maximum nesting depth setting + */ + void setMaxNestingDepth(const Sint32 maxDepth); + /* Get methods for SCP settings */ /** Returns TCP/IP port number SCP listens for new connection requests @@ -434,6 +445,14 @@ class DCMTK_DCMNET_EXPORT DcmSCP */ OFBool getProgressNotificationMode() const; + /** Return the maximum permitted sequence nesting depth for + * parsing datasets received over the network. + * @return maximum nesting depth setting + * (0 = compile-time default DCMTK_MAX_SEQUENCE_NESTING, + * -1 = unlimited) + */ + Sint32 getMaxNestingDepth() const; + /** Get access to the configuration of the SCP. Note that the functionality * on the configuration object is shadowed by other API functions of DcmSCP. * The existing functions are provided in order to not break users of this @@ -1194,6 +1213,11 @@ class DCMTK_DCMNET_EXPORT DcmSCP /// it, e.g. in the context of the DcmSCPPool class. DcmSharedSCPConfig m_cfg; + /// Maximum sequence nesting depth for parsing received datasets + /// (0 = compile-time default DCMTK_MAX_SEQUENCE_NESTING, + /// -1 = unlimited) + Sint32 m_maxNestingDepth; + /** Drops association and clears internal structures to free memory */ void dropAndDestroyAssociation();
dcmnet/include/dcmtk/dcmnet/scu.h+24 −0 modified@@ -860,6 +860,17 @@ class DCMTK_DCMNET_EXPORT DcmSCU */ void setProgressNotificationMode(const OFBool mode); + /** Set the maximum permitted sequence nesting depth for parsing + * datasets received over the network. This limit is applied to + * each dataset received via receiveDIMSEDataset(). + * - Value 0 (default): apply the compile-time default + * (DCMTK_MAX_SEQUENCE_NESTING) + * - Value -1: disable the check (allow unlimited nesting) + * - Value > 0: use this value as the maximum nesting depth + * @param maxDepth maximum nesting depth setting + */ + void setMaxNestingDepth(const Sint32 maxDepth); + /* Get methods */ /** Get current connection status @@ -952,6 +963,14 @@ class DCMTK_DCMNET_EXPORT DcmSCU */ OFBool getProgressNotificationMode() const; + /** Return the maximum permitted sequence nesting depth for + * parsing datasets received over the network. + * @return maximum nesting depth setting + * (0 = compile-time default DCMTK_MAX_SEQUENCE_NESTING, + * -1 = unlimited) + */ + Sint32 getMaxNestingDepth() const; + /** Returns whether SCU is configured to create a TLS connection with the SCP * @return OFTrue if TLS mode has been enabled, OFFalse otherwise */ @@ -1257,6 +1276,11 @@ class DCMTK_DCMNET_EXPORT DcmSCU /// Progress notification mode (default: enabled) OFBool m_progressNotificationMode; + /// Maximum sequence nesting depth for parsing received datasets + /// (0 = compile-time default DCMTK_MAX_SEQUENCE_NESTING, + /// -1 = unlimited) + Sint32 m_maxNestingDepth; + /// Flag indicating whether secure mode has been enabled (default: disabled) OFBool m_secureConnectionEnabled;
dcmnet/libsrc/dimse.cc+3 −2 modified@@ -1,6 +1,6 @@ /* * - * Copyright (C) 1994-2025, OFFIS e.V. + * Copyright (C) 1994-2026, OFFIS e.V. * All rights reserved. See COPYRIGHT file for details. * * This software and supporting documentation were partly developed by @@ -1536,7 +1536,8 @@ DIMSE_receiveDataSetInMemory( dset = *dataObject; } - /* check if there is still no DcmDataset object which can be used to store the data set. */ + /* check if there is still no DcmDataset object (i.e. if allocation fails) */ + /* which can be used to store the data set. */ if (dset == NULL) { /* if this is the case, just go ahead an receive data, but do not store it anywhere */
dcmnet/libsrc/scp.cc+36 −0 modified@@ -32,6 +32,7 @@ DcmSCP::DcmSCP() : m_network(NULL) , m_assoc(NULL) , m_cfg() +, m_maxNestingDepth(0) { OFStandard::initializeNetwork(); } @@ -1672,6 +1673,19 @@ OFCondition DcmSCP::receiveDIMSEDataset(T_ASC_PresentationContextID* presID, Dcm if (m_assoc == NULL) return DIMSE_ILLEGALASSOCIATION; + /* if a custom nesting depth limit is configured, pre-allocate + * the dataset and apply the setting before parsing starts. + * DIMSE_receiveDataSetInMemory() will use this dataset instead + * of creating a new one internally. */ + OFBool weAllocated = OFFalse; + if (dataObject != NULL && *dataObject == NULL + && m_maxNestingDepth != 0) + { + *dataObject = new DcmDataset(); + (*dataObject)->setMaxNestingDepth(m_maxNestingDepth); + weAllocated = OFTrue; + } + OFCondition cond; /* call the corresponding DIMSE function to receive the dataset */ if (m_cfg->getProgressNotificationMode()) @@ -1701,6 +1715,14 @@ OFCondition DcmSCP::receiveDIMSEDataset(T_ASC_PresentationContextID* presID, Dcm } else { + /* if we pre-allocated the dataset, clean up on error since + * DIMSE_receiveDataSetInMemory() won't delete a dataset + * that was passed in by the caller */ + if (weAllocated && dataObject != NULL) + { + delete *dataObject; + *dataObject = NULL; + } OFString tempStr; DCMNET_ERROR("Unable to receive dataset on presentation context " << OFstatic_cast(unsigned int, *presID) << ": " << DimseCondition::dump(tempStr, cond)); @@ -1992,6 +2014,13 @@ void DcmSCP::setAlwaysAcceptDefaultRole(const OFBool enabled) // ---------------------------------------------------------------------------- +void DcmSCP::setMaxNestingDepth(const Sint32 maxDepth) +{ + m_maxNestingDepth = maxDepth; +} + +// ---------------------------------------------------------------------------- + /* Get methods for SCP settings and current association information */ OFBool DcmSCP::getRefuseAssociation() const @@ -2085,6 +2114,13 @@ OFBool DcmSCP::getProgressNotificationMode() const // ---------------------------------------------------------------------------- +Sint32 DcmSCP::getMaxNestingDepth() const +{ + return m_maxNestingDepth; +} + +// ---------------------------------------------------------------------------- + OFBool DcmSCP::isConnected() const { return (m_assoc != NULL) && (m_assoc->DULassociation != NULL);
dcmnet/libsrc/scu.cc+32 −0 modified@@ -67,6 +67,7 @@ DcmSCU::DcmSCU() , m_verbosePCMode(OFFalse) , m_datasetConversionMode(OFFalse) , m_progressNotificationMode(OFTrue) + , m_maxNestingDepth(0) , m_secureConnectionEnabled(OFFalse) , m_protocolVersion(ASC_AF_Default) { @@ -2693,6 +2694,19 @@ OFCondition DcmSCU::receiveDIMSEDataset(T_ASC_PresentationContextID* presID, Dcm if (!isConnected()) return DIMSE_ILLEGALASSOCIATION; + /* if a custom nesting depth limit is configured, pre-allocate + * the dataset and apply the setting before parsing starts. + * DIMSE_receiveDataSetInMemory() will use this dataset instead + * of creating a new one internally. */ + OFBool weAllocated = OFFalse; + if (dataObject != NULL && *dataObject == NULL + && m_maxNestingDepth != 0) + { + *dataObject = new DcmDataset(); + (*dataObject)->setMaxNestingDepth(m_maxNestingDepth); + weAllocated = OFTrue; + } + OFCondition cond; /* call the corresponding DIMSE function to receive the dataset */ if (m_progressNotificationMode) @@ -2712,6 +2726,14 @@ OFCondition DcmSCU::receiveDIMSEDataset(T_ASC_PresentationContextID* presID, Dcm } else { + /* if we pre-allocated the dataset, clean up on error since + * DIMSE_receiveDataSetInMemory() won't delete a dataset + * that was passed in by the caller */ + if (weAllocated && dataObject != NULL) + { + delete *dataObject; + *dataObject = NULL; + } OFString tempStr; DCMNET_ERROR("Unable to receive dataset on presentation context " << OFstatic_cast(unsigned int, *presID) << ": " << DimseCondition::dump(tempStr, cond)); @@ -2800,6 +2822,11 @@ void DcmSCU::setProgressNotificationMode(const OFBool mode) m_progressNotificationMode = mode; } +void DcmSCU::setMaxNestingDepth(const Sint32 maxDepth) +{ + m_maxNestingDepth = maxDepth; +} + void DcmSCU::setProtocolVersion(T_ASC_ProtocolFamily protocolVersion) { m_protocolVersion = protocolVersion; @@ -2887,6 +2914,11 @@ OFBool DcmSCU::getProgressNotificationMode() const return m_progressNotificationMode; } +Sint32 DcmSCU::getMaxNestingDepth() const +{ + return m_maxNestingDepth; +} + OFCondition DcmSCU::getDatasetInfo(DcmDataset* dataset, OFString& sopClassUID, OFString& sopInstanceUID,
dcmnet/tests/tests.cc+5 −0 modified@@ -56,6 +56,11 @@ OFTEST_REGISTER(dcmnet_scu_sendNGETRequest_retrieves_all_attributes_when_list_is OFTEST_REGISTER(dcmnet_scu_sendNGETRequest_retrieves_only_requested_attributes); OFTEST_REGISTER(dcmnet_scu_sendNGETRequest_sets_error_status_for_nonexistent_instance); +OFTEST_REGISTER(dcmnet_scp_maxNestingDepth_getset); +OFTEST_REGISTER(dcmnet_scu_maxNestingDepth_getset); +OFTEST_REGISTER(dcmnet_scp_maxNestingDepth_rejects_deep); +OFTEST_REGISTER(dcmnet_scp_maxNestingDepth_accepts_shallow); + #endif // WITH_THREADS OFTEST_MAIN("dcmnet")
dcmnet/tests/tscuscp.cc+205 −0 modified@@ -1268,4 +1268,209 @@ OFTEST_FLAGS(dcmnet_scu_sendNGETRequest_sets_error_status_for_nonexistent_instan } +// Test that setMaxNestingDepth/getMaxNestingDepth work on DcmSCP +OFTEST(dcmnet_scp_maxNestingDepth_getset) +{ + DcmSCP scp; + // Default must be 0 (use compile-time default) + OFCHECK_EQUAL(scp.getMaxNestingDepth(), 0); + scp.setMaxNestingDepth(10); + OFCHECK_EQUAL(scp.getMaxNestingDepth(), 10); + scp.setMaxNestingDepth(-1); + OFCHECK_EQUAL(scp.getMaxNestingDepth(), -1); + scp.setMaxNestingDepth(0); + OFCHECK_EQUAL(scp.getMaxNestingDepth(), 0); +} + + +// Test that setMaxNestingDepth/getMaxNestingDepth work on DcmSCU +OFTEST(dcmnet_scu_maxNestingDepth_getset) +{ + DcmSCU scu; + // Default must be 0 (use compile-time default) + OFCHECK_EQUAL(scu.getMaxNestingDepth(), 0); + scu.setMaxNestingDepth(5); + OFCHECK_EQUAL(scu.getMaxNestingDepth(), 5); + scu.setMaxNestingDepth(-1); + OFCHECK_EQUAL(scu.getMaxNestingDepth(), -1); + scu.setMaxNestingDepth(0); + OFCHECK_EQUAL(scu.getMaxNestingDepth(), 0); +} + + +/** Helper: build a DcmDataset with nested sequences to a given depth. + * @param depth number of nesting levels + * @param dset output dataset (cleared before use) + */ +static void buildNestedDataset(Uint32 depth, DcmDataset& dset) +{ + dset.clear(); + DcmItem* currentItem = &dset; + for (Uint32 i = 0; i < depth; ++i) + { + DcmSequenceOfItems* sq = + new DcmSequenceOfItems(DCM_ReferencedSeriesSequence); + currentItem->insert(sq); + DcmItem* item = new DcmItem(); + sq->insert(item); + currentItem = item; + } +} + + +/** SCP that applies a nesting depth limit and records whether + * receiving the dataset succeeded or failed. + */ +struct NestingTestSCP : TestSCP +{ + NestingTestSCP(Sint32 maxNesting) + : TestSCP() + , m_receiveResult(EC_NotYetImplemented) + { + setMaxNestingDepth(maxNesting); + DcmSCPConfig& config = getConfig(); + config.setAETitle("NESTING_SCP"); + config.setConnectionBlockingMode(DUL_NOBLOCK); + config.setConnectionTimeout(10); + config.setHostLookupEnabled(OFFalse); + config.setPort(0); + OFList<OFString> xfers; + xfers.push_back(UID_LittleEndianImplicitTransferSyntax); + OFCHECK(config.addPresentationContext( + UID_ModalityPerformedProcedureStepSOPClass, xfers).good()); + OFCHECK(openListenPort().good()); + m_portNum = config.getPort(); + } + + OFCondition handleIncomingCommand( + T_DIMSE_Message* incomingMsg, + const DcmPresentationContextInfo& presInfo) + { + if (incomingMsg->CommandField == DIMSE_N_CREATE_RQ) + { + T_DIMSE_N_CreateRQ& req = incomingMsg->msg.NCreateRQ; + if (req.DataSetType != DIMSE_DATASET_NULL) + { + DcmDataset* dataset = OFnullptr; + T_ASC_PresentationContextID presIDdset; + m_receiveResult = + receiveDIMSEDataset(&presIDdset, &dataset); + /* send a response regardless of receive outcome */ + T_DIMSE_Message rsp; + memset(&rsp, 0, sizeof(rsp)); + rsp.CommandField = DIMSE_N_CREATE_RSP; + T_DIMSE_N_CreateRSP& createRsp = rsp.msg.NCreateRSP; + createRsp.MessageIDBeingRespondedTo = req.MessageID; + createRsp.DimseStatus = m_receiveResult.good() + ? STATUS_N_Success + : STATUS_N_ProcessingFailure; + createRsp.DataSetType = DIMSE_DATASET_NULL; + OFStandard::strlcpy( + createRsp.AffectedSOPClassUID, + req.AffectedSOPClassUID, + sizeof(createRsp.AffectedSOPClassUID)); + createRsp.opts = O_NCREATE_AFFECTEDSOPCLASSUID; + sendDIMSEMessage( + presInfo.presentationContextID, &rsp, NULL); + delete dataset; + } + return EC_Normal; + } + return DcmSCP::handleIncomingCommand(incomingMsg, presInfo); + } + + /// Result of receiveDIMSEDataset() for the last request + OFCondition m_receiveResult; + /// Port the SCP is listening on + Uint16 m_portNum; +}; + + +// Test that SCP's nesting depth limit rejects deeply nested data +// received over the network +OFTEST_FLAGS(dcmnet_scp_maxNestingDepth_rejects_deep, EF_Slow) +{ + NestingTestSCP scp(3); + scp.m_set_stop_after_assoc = OFTrue; + scp.start(); + OFStandard::forceSleep(1); + + // SCU sends a dataset with 4 levels of nesting (exceeds limit 3) + DcmSCU scu; + scu.setAETitle("NESTING_SCU"); + scu.setPeerAETitle("NESTING_SCP"); + scu.setPeerHostName("localhost"); + scu.setPeerPort(scp.m_portNum); + OFList<OFString> xfers; + xfers.push_back(UID_LittleEndianImplicitTransferSyntax); + OFCHECK(scu.addPresentationContext( + UID_ModalityPerformedProcedureStepSOPClass, xfers).good()); + OFCHECK(scu.initNetwork().good()); + OFCHECK(scu.negotiateAssociation().good()); + + DcmDataset reqDataset; + buildNestedDataset(4, reqDataset); + T_ASC_PresentationContextID presID = scu.findPresentationContextID( + UID_ModalityPerformedProcedureStepSOPClass, + UID_LittleEndianImplicitTransferSyntax); + OFCHECK(presID != 0); + Uint16 rspStatus = 0; + DcmDataset* rspDataset = NULL; + scu.sendNCREATERequest(presID, "1.2.3.4.5", + &reqDataset, rspDataset, rspStatus); + delete rspDataset; + if (scu.isConnected()) + scu.releaseAssociation(); + OFStandard::forceSleep(2); + scp.join(); + + // Verify SCP saw the nesting depth error + OFCHECK(scp.m_receiveResult == DIMSE_RECEIVEFAILED); +} + + +// Test that SCP's nesting depth limit accepts data within the limit +OFTEST_FLAGS(dcmnet_scp_maxNestingDepth_accepts_shallow, EF_Slow) +{ + NestingTestSCP scp(3); + scp.m_set_stop_after_assoc = OFTrue; + scp.start(); + OFStandard::forceSleep(1); + + // SCU sends a dataset with 3 levels of nesting (at the limit) + DcmSCU scu; + scu.setAETitle("NESTING_SCU"); + scu.setPeerAETitle("NESTING_SCP"); + scu.setPeerHostName("localhost"); + scu.setPeerPort(scp.m_portNum); + OFList<OFString> xfers; + xfers.push_back(UID_LittleEndianImplicitTransferSyntax); + OFCHECK(scu.addPresentationContext( + UID_ModalityPerformedProcedureStepSOPClass, xfers).good()); + OFCHECK(scu.initNetwork().good()); + OFCHECK(scu.negotiateAssociation().good()); + + DcmDataset reqDataset; + buildNestedDataset(3, reqDataset); + T_ASC_PresentationContextID presID = scu.findPresentationContextID( + UID_ModalityPerformedProcedureStepSOPClass, + UID_LittleEndianImplicitTransferSyntax); + OFCHECK(presID != 0); + Uint16 rspStatus = 0; + DcmDataset* rspDataset = NULL; + OFCondition result = scu.sendNCREATERequest(presID, "1.2.3.4.5", + &reqDataset, rspDataset, rspStatus); + OFCHECK_MSG(result.good(), result.text()); + OFCHECK(rspStatus == STATUS_N_Success); + delete rspDataset; + if (scu.isConnected()) + OFCHECK(scu.releaseAssociation().good()); + OFStandard::forceSleep(2); + scp.join(); + + // Verify SCP successfully received the dataset + OFCHECK(scp.m_receiveResult.good()); +} + + #endif // WITH_THREADS
Vulnerability mechanics
Root cause
"The DCMTK parser did not properly limit the depth of nested DICOM sequences, allowing for excessively deep nesting that could exhaust stack memory."
Attack vector
An attacker must have local access to the affected system. By crafting a DICOM file with deeply nested sequences, the attacker can trigger a stack-based buffer overflow when the file is parsed by the DCMTK component within Orthanc [ref_id=1]. This overflow can lead to a denial of service.
Affected code
The vulnerability lies within the DCMTK parser, specifically in how it handles nested DICOM sequences. The affected code paths include DcmInputStream and DcmItem, where the nesting depth was not adequately tracked or limited, leading to potential stack overflows during parsing operations like `DcmItem::read()` [ref_id=1].
What the fix does
The patch introduces a sequence nesting depth limit to the DCMTK parser. This limit is enforced during the parsing of DICOM data, preventing excessively nested sequences from causing a stack overflow [patch_id=4433600]. The default limit is set to 64, but it can be configured at runtime, and a value of -1 disables the check entirely. This prevents the unbounded recursion that could lead to a denial of service.
Preconditions
- inputThe attacker must provide a specially crafted DICOM file with deeply nested sequences.
- networkLocal access to the affected system is required.
Generated on Jun 2, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
8News mentions
0No linked articles in our index yet.