From 6456a3ede2d3b0a8d6343301030f580ecbd225f5 Mon Sep 17 00:00:00 2001 From: Michael Onken Date: Wed, 3 Jun 2026 21:54:21 +0200 Subject: [PATCH] Add sequence nesting depth limit for parsing MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Applied-Upstream: 885ff0f10372bd589b5f44cea974f28a3964cb0f Last-Update: 2026-04-11 Bug: https://support.dcmtk.org/redmine/issues/1191 Bug-Debian: https://bugs.debian.org/1138713 Reviewed-By: Étienne Mollier Track sequence nesting depth on DcmInputStream during parsing. When the depth exceeds a configurable limit, parsing is aborted with EC_NestingDepthLimitExceeded. This prevents stack overflow from excessively nested DICOM sequences. The default limit is controlled by the compile-time macro DCMTK_MAX_SEQUENCE_NESTING (default: 64). It can be overridden per parse operation via the runtime API: - DcmInputStream::setMaxNestingDepth() for direct stream access - DcmItem::setMaxNestingDepth() (inherited by DcmDataset) for loadFile() and read() - DcmFileFormat::setMaxNestingDepth() for loadFile() and read(), forwarded to the contained dataset - DcmSCP::setMaxNestingDepth() and DcmSCU::setMaxNestingDepth() for datasets received over the network via receiveDIMSEDataset() The runtime setter uses Sint32 semantics: 0 = compile-time default, -1 = unlimited, >0 = limit Note: the specific EC_NestingDepthLimitExceeded error code is not yet surfaced through the DIMSE layer; DIMSE_receiveDataSetInMemory() maps all parse errors to DIMSE_RECEIVEFAILED. Thanks to the IN-CYPHER OSS Security Team for the report, detailed analysis and proof of concept. This closes DCMTK Bug #1191. Gbp-Pq: Name CVE-2026-10528-partial.patch --- config/docs/macros.txt | 15 ++ dcmdata/include/dcmtk/dcmdata/dcerror.h | 4 +- dcmdata/include/dcmtk/dcmdata/dcfilefo.h | 29 +++ dcmdata/include/dcmtk/dcmdata/dcistrma.h | 42 +++- dcmdata/include/dcmtk/dcmdata/dcitem.h | 25 ++- dcmdata/include/dcmtk/dcmdata/dcobject.h | 14 +- dcmdata/libsrc/dcdatset.cc | 6 + dcmdata/libsrc/dcerror.cc | 1 + dcmdata/libsrc/dcfilefo.cc | 16 +- dcmdata/libsrc/dcistrma.cc | 30 ++- dcmdata/libsrc/dcitem.cc | 14 +- dcmdata/libsrc/dcsequen.cc | 20 +- dcmdata/tests/CMakeLists.txt | 1 + dcmdata/tests/Makefile.dep | 67 ++++++ dcmdata/tests/Makefile.in | 2 +- dcmdata/tests/tests.cc | 9 +- dcmdata/tests/tnesting.cc | 266 +++++++++++++++++++++++ dcmnet/include/dcmtk/dcmnet/scp.h | 24 ++ dcmnet/include/dcmtk/dcmnet/scu.h | 24 ++ dcmnet/libsrc/dimse.cc | 5 +- dcmnet/libsrc/scp.cc | 36 +++ dcmnet/libsrc/scu.cc | 32 +++ dcmnet/tests/tests.cc | 5 + dcmnet/tests/tscuscp.cc | 205 +++++++++++++++++ 24 files changed, 876 insertions(+), 16 deletions(-) create mode 100644 dcmdata/tests/tnesting.cc diff --git a/config/docs/macros.txt b/config/docs/macros.txt index cd9ff2f0..b44cd166 100644 --- a/config/docs/macros.txt +++ b/config/docs/macros.txt @@ -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 diff --git a/dcmdata/include/dcmtk/dcmdata/dcerror.h b/dcmdata/include/dcmtk/dcmdata/dcerror.h index c8ba944b..96533acb 100644 --- a/dcmdata/include/dcmtk/dcmdata/dcerror.h +++ b/dcmdata/include/dcmtk/dcmdata/dcerror.h @@ -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; ///@} diff --git a/dcmdata/include/dcmtk/dcmdata/dcfilefo.h b/dcmdata/include/dcmtk/dcmdata/dcfilefo.h index 452aa1dd..18f5aec1 100644 --- a/dcmdata/include/dcmtk/dcmdata/dcfilefo.h +++ b/dcmdata/include/dcmtk/dcmdata/dcfilefo.h @@ -514,6 +514,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; } }; diff --git a/dcmdata/include/dcmtk/dcmdata/dcistrma.h b/dcmdata/include/dcmtk/dcmdata/dcistrma.h index f9ce25b9..0d190238 100644 --- a/dcmdata/include/dcmtk/dcmdata/dcistrma.h +++ b/dcmdata/include/dcmtk/dcmdata/dcistrma.h @@ -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 @@ public: */ 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 @@ private: /// 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_; }; diff --git a/dcmdata/include/dcmtk/dcmdata/dcitem.h b/dcmdata/include/dcmtk/dcmdata/dcitem.h index 84d2b6f3..5b4a3ff8 100644 --- a/dcmdata/include/dcmtk/dcmdata/dcitem.h +++ b/dcmdata/include/dcmtk/dcmdata/dcitem.h @@ -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 diff --git a/dcmdata/include/dcmtk/dcmdata/dcobject.h b/dcmdata/include/dcmtk/dcmdata/dcobject.h index 714520bc..9a1499cc 100644 --- a/dcmdata/include/dcmtk/dcmdata/dcobject.h +++ b/dcmdata/include/dcmtk/dcmdata/dcobject.h @@ -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; diff --git a/dcmdata/libsrc/dcdatset.cc b/dcmdata/libsrc/dcdatset.cc index 9292f139..375d384e 100644 --- a/dcmdata/libsrc/dcdatset.cc +++ b/dcmdata/libsrc/dcdatset.cc @@ -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(); diff --git a/dcmdata/libsrc/dcerror.cc b/dcmdata/libsrc/dcerror.cc index a27c93b5..33030367 100644 --- a/dcmdata/libsrc/dcerror.cc +++ b/dcmdata/libsrc/dcerror.cc @@ -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; diff --git a/dcmdata/libsrc/dcfilefo.cc b/dcmdata/libsrc/dcfilefo.cc index b1a8c16c..d4a91e67 100644 --- a/dcmdata/libsrc/dcfilefo.cc +++ b/dcmdata/libsrc/dcfilefo.cc @@ -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(); DcmSequenceOfItems::itemList->insert(MetaInfo); @@ -99,7 +101,8 @@ DcmFileFormat::DcmFileFormat(const DcmFileFormat &old) : DcmSequenceOfItems(old), FileReadMode(old.FileReadMode), ImplementationClassUID(old.ImplementationClassUID), - ImplementationVersionName(old.ImplementationVersionName) + ImplementationVersionName(old.ImplementationVersionName), + MaxNestingDepth(old.MaxNestingDepth) { } @@ -128,6 +131,7 @@ DcmFileFormat &DcmFileFormat::operator=(const DcmFileFormat &obj) FileReadMode = obj.FileReadMode; ImplementationClassUID = obj.ImplementationClassUID; ImplementationVersionName = obj.ImplementationVersionName; + MaxNestingDepth = obj.MaxNestingDepth; } return *this; @@ -942,6 +946,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(); @@ -972,6 +979,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(); diff --git a/dcmdata/libsrc/dcistrma.cc b/dcmdata/libsrc/dcistrma.cc index b61a79b7..efc446a6 100644 --- a/dcmdata/libsrc/dcistrma.cc +++ b/dcmdata/libsrc/dcistrma.cc @@ -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; diff --git a/dcmdata/libsrc/dcitem.cc b/dcmdata/libsrc/dcitem.cc index 62bf16f2..274ad14e 100644 --- a/dcmdata/libsrc/dcitem.cc +++ b/dcmdata/libsrc/dcitem.cc @@ -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()) { @@ -136,6 +139,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); @@ -1394,6 +1398,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 */ diff --git a/dcmdata/libsrc/dcsequen.cc b/dcmdata/libsrc/dcsequen.cc index adeb6453..e10c375a 100644 --- a/dcmdata/libsrc/dcsequen.cc +++ b/dcmdata/libsrc/dcsequen.cc @@ -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 @@ -704,6 +704,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()) @@ -773,6 +790,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()); diff --git a/dcmdata/tests/CMakeLists.txt b/dcmdata/tests/CMakeLists.txt index 98ea562e..607e5ad5 100644 --- a/dcmdata/tests/CMakeLists.txt +++ b/dcmdata/tests/CMakeLists.txt @@ -11,6 +11,7 @@ DCMTK_ADD_TEST_EXECUTABLE(dcmdata_tests ti2dbmp.cc titem.cc tmatch.cc + tnesting.cc tnewdcme.cc tparent.cc tparser.cc diff --git a/dcmdata/tests/Makefile.dep b/dcmdata/tests/Makefile.dep index 800b37b2..fdfc723d 100644 --- a/dcmdata/tests/Makefile.dep +++ b/dcmdata/tests/Makefile.dep @@ -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 \ diff --git a/dcmdata/tests/Makefile.in b/dcmdata/tests/Makefile.in index 03312127..cfdd0ba8 100644 --- a/dcmdata/tests/Makefile.in +++ b/dcmdata/tests/Makefile.in @@ -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 diff --git a/dcmdata/tests/tests.cc b/dcmdata/tests/tests.cc index c8b40d42..6efd4883 100644 --- a/dcmdata/tests/tests.cc +++ b/dcmdata/tests/tests.cc @@ -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") diff --git a/dcmdata/tests/tnesting.cc b/dcmdata/tests/tnesting.cc new file mode 100644 index 00000000..5b89f322 --- /dev/null +++ b/dcmdata/tests/tnesting.cc @@ -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); +} diff --git a/dcmnet/include/dcmtk/dcmnet/scp.h b/dcmnet/include/dcmtk/dcmnet/scp.h index 316ec7b1..fdf73d3d 100644 --- a/dcmnet/include/dcmtk/dcmnet/scp.h +++ b/dcmnet/include/dcmtk/dcmnet/scp.h @@ -360,6 +360,17 @@ public: */ 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 @@ public: */ 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 @@ -1157,6 +1176,11 @@ private: /// 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(); diff --git a/dcmnet/include/dcmtk/dcmnet/scu.h b/dcmnet/include/dcmtk/dcmnet/scu.h index 5a83c25a..8fcb1786 100644 --- a/dcmnet/include/dcmtk/dcmnet/scu.h +++ b/dcmnet/include/dcmtk/dcmnet/scu.h @@ -739,6 +739,17 @@ public: */ 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 @@ -831,6 +842,14 @@ public: */ 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 */ @@ -1136,6 +1155,11 @@ private: /// 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; diff --git a/dcmnet/libsrc/dimse.cc b/dcmnet/libsrc/dimse.cc index 38d011ff..28da484a 100644 --- a/dcmnet/libsrc/dimse.cc +++ b/dcmnet/libsrc/dimse.cc @@ -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 */ diff --git a/dcmnet/libsrc/scp.cc b/dcmnet/libsrc/scp.cc index 0b84d00b..bfb336db 100644 --- a/dcmnet/libsrc/scp.cc +++ b/dcmnet/libsrc/scp.cc @@ -32,6 +32,7 @@ DcmSCP::DcmSCP() : m_network(NULL) , m_assoc(NULL) , m_cfg() +, m_maxNestingDepth(0) { OFStandard::initializeNetwork(); } @@ -1590,6 +1591,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()) @@ -1619,6 +1633,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)); @@ -1910,6 +1932,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 @@ -2003,6 +2032,13 @@ OFBool DcmSCP::getProgressNotificationMode() const // ---------------------------------------------------------------------------- +Sint32 DcmSCP::getMaxNestingDepth() const +{ + return m_maxNestingDepth; +} + +// ---------------------------------------------------------------------------- + OFBool DcmSCP::isConnected() const { return (m_assoc != NULL) && (m_assoc->DULassociation != NULL); diff --git a/dcmnet/libsrc/scu.cc b/dcmnet/libsrc/scu.cc index fa3a8635..9fe00233 100644 --- a/dcmnet/libsrc/scu.cc +++ b/dcmnet/libsrc/scu.cc @@ -56,6 +56,7 @@ DcmSCU::DcmSCU() , m_verbosePCMode(OFFalse) , m_datasetConversionMode(OFFalse) , m_progressNotificationMode(OFTrue) + , m_maxNestingDepth(0) , m_secureConnectionEnabled(OFFalse) , m_protocolVersion(ASC_AF_Default) { @@ -2500,6 +2501,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) @@ -2519,6 +2533,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)); @@ -2607,6 +2629,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; @@ -2694,6 +2721,11 @@ OFBool DcmSCU::getProgressNotificationMode() const return m_progressNotificationMode; } +Sint32 DcmSCU::getMaxNestingDepth() const +{ + return m_maxNestingDepth; +} + OFCondition DcmSCU::getDatasetInfo(DcmDataset* dataset, OFString& sopClassUID, OFString& sopInstanceUID, diff --git a/dcmnet/tests/tests.cc b/dcmnet/tests/tests.cc index e7a2b438..67aa12e0 100644 --- a/dcmnet/tests/tests.cc +++ b/dcmnet/tests/tests.cc @@ -51,6 +51,11 @@ OFTEST_REGISTER(dcmnet_scu_sendNSETRequest_fails_when_requestedsopinstance_is_em OFTEST_REGISTER(dcmnet_scu_sendNSETRequest_succeeds_and_modifies_instance_when_scp_has_instance); OFTEST_REGISTER(dcmnet_scu_sendNSETRequest_succeeds_and_sets_responsestatuscode_from_scp_when_scp_sets_error_status); +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") diff --git a/dcmnet/tests/tscuscp.cc b/dcmnet/tests/tscuscp.cc index 3bf0b260..e5a83a02 100644 --- a/dcmnet/tests/tscuscp.cc +++ b/dcmnet/tests/tscuscp.cc @@ -1113,4 +1113,209 @@ OFTEST_FLAGS(dcmnet_scu_sendNSETRequest_succeeds_and_sets_responsestatuscode_fro } +// 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 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 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 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 -- 2.30.2