[med-svn] [Git][med-team/dcmtk][debian/trixie] 2 commits: d/patches/*-CVE-2026-*.patch: new security patches.

Étienne Mollier (@emollier) gitlab at salsa.debian.org
Tue Jul 7 21:41:56 BST 2026



Étienne Mollier pushed to branch debian/trixie at Debian Med / dcmtk


Commits:
3e1f2608 by Étienne Mollier at 2026-07-07T22:37:27+02:00
d/patches/*-CVE-2026-*.patch: new security patches.

This change includes a patch queue addressing CVE-2026-50003,
CVE-2026-50254, CVE-2026-35505, CVE-2026-52868 and CVE-2026-44628.
The latter CVE-2026-44628 is divided into two patches to match
upstream's commits.  These changes fix a range of issues, including
risks of path traversals, denial of services and information leaks.

Closes: #1141411

- - - - -
988ba8fa by Étienne Mollier at 2026-07-07T22:40:04+02:00
d/changelog: ready for upload to trixie-pu.

- - - - -


8 changed files:

- debian/changelog
- + debian/patches/0020-CVE-2026-50003.patch
- + debian/patches/0021-CVE-2026-50254.patch
- + debian/patches/0022-CVE-2026-35505.patch
- + debian/patches/0023-CVE-2026-52868.patch
- + debian/patches/0024-CVE-2026-44628a.patch
- + debian/patches/0025-CVE-2026-44628b.patch
- debian/patches/series


Changes:

=====================================
debian/changelog
=====================================
@@ -1,3 +1,15 @@
+dcmtk (3.6.9-5+deb13u3) trixie; urgency=medium
+
+  * d/patches/*-CVE-2026-*.patch: new security patches.
+    This change includes a patch queue addressing CVE-2026-50003,
+    CVE-2026-50254, CVE-2026-35505, CVE-2026-52868 and CVE-2026-44628.
+    The latter CVE-2026-44628 is divided into two patches to match
+    upstream's commits.  These changes fix a range of issues, including
+    risks of path traversals, denial of services and information leaks.
+    (Closes: #1141411)
+
+ -- Étienne Mollier <emollier at debian.org>  Tue, 07 Jul 2026 22:38:06 +0200
+
 dcmtk (3.6.9-5+deb13u2) trixie; urgency=medium
 
   * Team upload.


=====================================
debian/patches/0020-CVE-2026-50003.patch
=====================================
@@ -0,0 +1,42 @@
+Applied-Upstream: eca9a03dda7d4fc1faa7e5a6dac9617938cf5803
+Author: Michael Onken <onken at open-connections.de>
+Last-Update: 2026-05-12
+Description: Fixed path traversal in DcmSCU bit-pres. C-GET.
+ In DCMSCU_STORAGE_BIT_PRESERVING mode, the C-STORE sub-operation
+ handler in handleCGETSession() built the on-disk filename from the
+ peer-supplied AffectedSOPInstanceUID without sanitization, allowing
+ a malicious C-STORE SCP to write files outside the configured
+ storage directory via path-separator or "../" sequences in the UID.
+ .
+ The DISK mode path was already sanitized (via createStorageFilename(),
+ fixed in commit f06a86751 for CVE-2022-2120); this branch was missed.
+ The fix mirrors the same pattern (sanitize a local OFString copy) so
+ the request struct stays intact and the C-STORE response still echoes
+ the peer's original UID per protocol.
+ .
+ Affects all consumers of DcmSCU using DCMSCU_STORAGE_BIT_PRESERVING,
+ including getscu --bit-preserving.
+ .
+ This fixes DCMTK issue #1207.
+ .
+ Thanks to Abhinav Agarwal for the report and analysis.
+Reviewed-By: Étienne Mollier <emollier at debian.org>
+Bug-Debian: https://bugs.debian.org/1141411
+
+--- dcmtk.orig/dcmnet/libsrc/scu.cc
++++ dcmtk/dcmnet/libsrc/scu.cc
+@@ -1220,9 +1220,13 @@
+             // handle bit preserving storage mode, i.e. receive directly to disk
+             else if (m_storageMode == DCMSCU_STORAGE_BIT_PRESERVING)
+             {
++                // Sanitize SOP Instance UID before using it as part of a filename
++                // to prevent path traversal via malicious peers (see also CVE-2022-2120).
++                OFString uidForFilename = rsp.msg.CStoreRQ.AffectedSOPInstanceUID;
++                OFStandard::sanitizeFilename(uidForFilename);
+                 OFString storageFilename;
+                 OFStandard::combineDirAndFilename(
+-                    storageFilename, m_storageDir, rsp.msg.CStoreRQ.AffectedSOPInstanceUID, OFTrue);
++                    storageFilename, m_storageDir, uidForFilename, OFTrue);
+                 result = handleSTORERequestFile(&pcid, storageFilename, &(rsp.msg.CStoreRQ));
+                 if (result.good())
+                 {


=====================================
debian/patches/0021-CVE-2026-50254.patch
=====================================
@@ -0,0 +1,164 @@
+Applied-Upstream: 23f181f7a3cb8334056f751a3a0c2ddf01046752
+Author: Michael Onken <onken at open-connections.de>
+Last-Update: 2026-05-26
+Description: Fix extNegList leaks on A-ASSOCIATE parse failure.
+ Thanks for the report and analysis to Abhinav Agarwal.
+ .
+ This closes DCMTK feature #126.
+Reviewed-By: Étienne Mollier <emollier at debian.org>
+Bug-Debian: https://bugs.debian.org/1141411
+
+--- dcmtk.orig/dcmnet/libsrc/dulconst.cc
++++ dcmtk/dcmnet/libsrc/dulconst.cc
+@@ -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 partly developed by
+@@ -1087,6 +1087,14 @@
+     unsigned long length;
+     *rtnLength = 0;
+ 
++    /* The transient list built here ends up as userInfo->extNegList of the
++     * outgoing PDU; via the shallow appendList() it borrows the sub-item
++     * pointers from the params-owned list. destroyUserInformationLists()
++     * (called once the PDU has been streamed) releases only the container,
++     * leaving the items in place for DUL_ClearServiceParameters() to free
++     * through the params side. See the ownership note in
++     * destroyUserInformationLists() (helpers.cc).
++     */
+     if (type == DUL_TYPEASSOCIATERQ && params->requestedExtNegList != NULL) {
+         *lst = new SOPClassExtendedNegotiationSubItemList;
+         if (*lst == NULL) return EC_MemoryExhausted;
+--- dcmtk.orig/dcmnet/libsrc/dulfsm.cc
++++ dcmtk/dcmnet/libsrc/dulfsm.cc
+@@ -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 partly developed by
+@@ -1031,7 +1031,14 @@
+ 
+         }
+ 
+-        /* extended negotiation */
++        /* Extended negotiation: ownership of the sub-item objects transfers
++         * from the transient assoc.userInfo.extNegList to service->acceptedExtNegList
++         * via the shallow appendList(). The destroyUserInformationLists() call
++         * a few lines below deliberately frees only the list container; the
++         * items themselves will be released by DUL_ClearServiceParameters()
++         * through the service parameter list. See the ownership note in
++         * destroyUserInformationLists() (helpers.cc).
++         */
+         if (assoc.userInfo.extNegList != NULL) {
+             service->acceptedExtNegList = new SOPClassExtendedNegotiationSubItemList;
+             if (service->acceptedExtNegList == NULL)  return EC_MemoryExhausted;
+@@ -1235,7 +1242,14 @@
+             return DUL_PCTRANSLATIONFAILURE;
+         }
+ 
+-        /* extended negotiation */
++        /* Extended negotiation: ownership of the sub-item objects transfers
++         * from the transient assoc.userInfo.extNegList to service->requestedExtNegList
++         * via the shallow appendList(). The destroyUserInformationLists() call
++         * a few lines below frees only the list container; the items themselves
++         * are released later by DUL_ClearServiceParameters() through the service
++         * parameter list. See the ownership note in destroyUserInformationLists()
++         * (helpers.cc).
++         */
+         if (assoc.userInfo.extNegList != NULL) {
+             service->requestedExtNegList = new SOPClassExtendedNegotiationSubItemList;
+             if (service->requestedExtNegList == NULL) return EC_MemoryExhausted;
+--- dcmtk.orig/dcmnet/libsrc/dulparse.cc
++++ dcmtk/dcmnet/libsrc/dulparse.cc
+@@ -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 partly developed by
+@@ -292,6 +292,15 @@
+     if (cond.bad())
+     {
+       destroyAssociatePDUPresentationContextList(&assoc->presentationContextList);
++      /* On a parse error the parsed extended negotiation sub-items are only
++       * held by assoc->userInfo.extNegList; no service parameter list has
++       * adopted them yet. destroyUserInformationLists() releases the list
++       * container but not its members (see the ownership note there), so
++       * the SOPClassExtendedNegotiationSubItem objects must be released
++       * explicitly here.
++       */
++      if (assoc->userInfo.extNegList != NULL)
++        deleteListMembers(*assoc->userInfo.extNegList);
+       destroyUserInformationLists(&assoc->userInfo);
+     }
+     return cond;
+@@ -590,7 +599,17 @@
+             extNeg = new SOPClassExtendedNegotiationSubItem;
+             if (extNeg == NULL)  return EC_MemoryExhausted;
+             cond = parseExtNeg(extNeg, buf, &length, userLength);
+-            if (cond.bad()) return cond;
++            if (cond.bad())
++            {
++                /* extNeg has not yet been pushed to extNegList, so the
++                 * outer cleanup in parseAssociate would not see it. Release
++                 * it here. parseExtNeg only returns errors before allocating
++                 * extNeg->serviceClassAppInfo, so there is no inner buffer
++                 * to free.
++                 */
++                delete extNeg;
++                return cond;
++            }
+             if (userInfo->extNegList == NULL)
+             {
+                 userInfo->extNegList = new SOPClassExtendedNegotiationSubItemList;
+--- dcmtk.orig/dcmnet/libsrc/extneg.cc
++++ dcmtk/dcmnet/libsrc/extneg.cc
+@@ -14,6 +14,12 @@
+ #include "dcmtk/dcmnet/extneg.h"
+ 
+ 
++/* Shallow copy: 'to' receives the same SOPClassExtendedNegotiationSubItem*
++ * pointers held by 'from'. Both lists end up sharing the items; exactly one
++ * side may eventually call deleteListMembers() on them. See the ownership
++ * note in destroyUserInformationLists() (helpers.cc) for which side that is
++ * on each call path.
++ */
+ void appendList(const SOPClassExtendedNegotiationSubItemList& from, SOPClassExtendedNegotiationSubItemList& to)
+ {
+     OFListConstIterator(SOPClassExtendedNegotiationSubItem*) i = from.begin();
+--- dcmtk.orig/dcmnet/libsrc/helpers.cc
++++ dcmtk/dcmnet/libsrc/helpers.cc
+@@ -1,6 +1,6 @@
+ /*
+  *
+- *  Copyright (C) 2021, OFFIS e.V.
++ *  Copyright (C) 2021-2026, OFFIS e.V.
+  *  All rights reserved.  See COPYRIGHT file for details.
+  *
+  *  This software and supporting documentation were partly developed by
+@@ -63,7 +63,15 @@
+     }
+     LST_Destroy(&userInfo->SCUSCPRoleList);
+ 
+-    /* extended negotiation */
++    /* Extended negotiation: the list contents (SOPClassExtendedNegotiationSubItem*)
++     * are intentionally NOT deleted here. On all normal paths the items are
++     * shared (via the shallow appendList()) with the owning service parameter
++     * list (params->{requested,accepted}ExtNegList in DUL_ASSOCIATESERVICEPARAMETERS),
++     * which releases them through DUL_ClearServiceParameters() ->
++     * deleteListMembers(). Callers with no service-side owner (e.g. the
++     * parse-error cleanup in parseAssociate()) must call deleteListMembers()
++     * themselves before invoking this function.
++     */
+     delete userInfo->extNegList;
+     userInfo->extNegList = NULL;
+ 


=====================================
debian/patches/0022-CVE-2026-35505.patch
=====================================
@@ -0,0 +1,509 @@
+Applied-Upstream: 2312891a8d058c862e00bcbd636e5da26308658a
+Author: Michael Onken <onken at open-connections.de>
+Last-Update: 2026-05-29
+Description: Fix A-ASSOCIATE-RQ/AC error-path leaks in DUL FSM.
+ AE_3_AssociateConfirmationAccept and AE_6_ExamineAssociateRequest leaked
+ the whole parsed PDU graph (presentation contexts, transfer syntax
+ sub-chains, role list, user info) on error returns taken after
+ parseAssociate() succeeded, since only the success path freed it. For an
+ SCP this is remotely triggerable and accumulates per connection.
+ .
+ Free the parsed PDU before each such return;
+ translatePresentationContextList() also frees the proposed transfer
+ syntax list of a rejected context. Add a dcmnet regression test
+ (tassocleak.cc) driving the AE_6 zero-transfer-syntax path against an
+ in-process DcmSCP; leak-clean with the fix under DCMTK_WITH_SANITIZERS.
+ .
+ Thanks for the report and analysis to Abhinav Agarwal.
+ .
+ This closes DCMTK issue #1217.
+Reviewed-By: Étienne Mollier <emollier at debian.org>
+Bug-Debian: https://bugs.debian.org/1141411
+
+--- dcmtk.orig/dcmnet/libsrc/dulfsm.cc
++++ dcmtk/dcmnet/libsrc/dulfsm.cc
+@@ -971,14 +971,26 @@
+         OFStandard::strlcpy(service->callingAPTitle, assoc.callingAPTitle, sizeof(service->callingAPTitle));
+         OFStandard::strlcpy(service->applicationContextName, assoc.applicationContext.data, sizeof(service->applicationContextName));
+ 
+-        if ((service->acceptedPresentationContext = LST_Create()) == NULL) return EC_MemoryExhausted;
++        if ((service->acceptedPresentationContext = LST_Create()) == NULL)
++        {
++            /* free memory allocated by parseAssociate() before returning */
++            destroyAssociatePDUPresentationContextList(&assoc.presentationContextList);
++            destroyUserInformationLists(&assoc.userInfo);
++            return EC_MemoryExhausted;
++        }
+ 
+         prvCtx = (PRV_PRESENTATIONCONTEXTITEM*)LST_Head(&assoc.presentationContextList);
+         if (prvCtx != NULL)
+             (void) LST_Position(&assoc.presentationContextList, (LST_NODE*)prvCtx);
+         while (prvCtx != NULL) {
+             userPresentationCtx = (DUL_PRESENTATIONCONTEXT*)malloc(sizeof(DUL_PRESENTATIONCONTEXT));
+-            if (userPresentationCtx == NULL) return EC_MemoryExhausted;
++            if (userPresentationCtx == NULL)
++            {
++                /* free memory allocated by parseAssociate() before returning */
++                destroyAssociatePDUPresentationContextList(&assoc.presentationContextList);
++                destroyUserInformationLists(&assoc.userInfo);
++                return EC_MemoryExhausted;
++            }
+ 
+             (void) memset(userPresentationCtx, 0, sizeof(DUL_PRESENTATIONCONTEXT));
+             userPresentationCtx->result = prvCtx->result;
+@@ -1011,6 +1023,9 @@
+               char buf1[256];
+               OFStandard::snprintf(buf1, sizeof(buf1), "DUL Peer supplied illegal number of transfer syntaxes (%d)", 0);
+               free(userPresentationCtx);
++              /* free memory allocated by parseAssociate() before returning */
++              destroyAssociatePDUPresentationContextList(&assoc.presentationContextList);
++              destroyUserInformationLists(&assoc.userInfo);
+               return makeDcmnetCondition(DULC_PEERILLEGALXFERSYNTAXCOUNT, OF_error, buf1);
+             }
+ 
+@@ -1019,6 +1034,9 @@
+               char buf2[256];
+               OFStandard::snprintf(buf2, sizeof(buf2), "DUL Peer supplied illegal number of transfer syntaxes (%ld)", LST_Count(&prvCtx->transferSyntaxList));
+               free(userPresentationCtx);
++              /* free memory allocated by parseAssociate() before returning */
++              destroyAssociatePDUPresentationContextList(&assoc.presentationContextList);
++              destroyUserInformationLists(&assoc.userInfo);
+               return makeDcmnetCondition(DULC_PEERILLEGALXFERSYNTAXCOUNT, OF_error, buf2);
+             }
+             subItem = (DUL_SUBITEM*)LST_Head(&prvCtx->transferSyntaxList);
+@@ -1041,7 +1059,13 @@
+          */
+         if (assoc.userInfo.extNegList != NULL) {
+             service->acceptedExtNegList = new SOPClassExtendedNegotiationSubItemList;
+-            if (service->acceptedExtNegList == NULL)  return EC_MemoryExhausted;
++            if (service->acceptedExtNegList == NULL)
++            {
++                /* free memory allocated by parseAssociate() before returning */
++                destroyAssociatePDUPresentationContextList(&assoc.presentationContextList);
++                destroyUserInformationLists(&assoc.userInfo);
++                return EC_MemoryExhausted;
++            }
+             appendList(*assoc.userInfo.extNegList, *service->acceptedExtNegList);
+         }
+ 
+@@ -1049,7 +1073,13 @@
+         if (assoc.userInfo.usrIdent != NULL) {
+           service->ackUserIdentNeg =
+             new UserIdentityNegotiationSubItemAC( *(OFstatic_cast(UserIdentityNegotiationSubItemAC*, assoc.userInfo.usrIdent)));
+-          if (service->ackUserIdentNeg == NULL)  return EC_MemoryExhausted;
++          if (service->ackUserIdentNeg == NULL)
++          {
++              /* free memory allocated by parseAssociate() before returning */
++              destroyAssociatePDUPresentationContextList(&assoc.presentationContextList);
++              destroyUserInformationLists(&assoc.userInfo);
++              return EC_MemoryExhausted;
++          }
+ 
+         }
+ 
+@@ -1234,11 +1264,20 @@
+         OFStandard::strlcpy(service->callingAPTitle, assoc.callingAPTitle, sizeof(service->callingAPTitle));
+         OFStandard::strlcpy(service->applicationContextName, assoc.applicationContext.data, sizeof(service->applicationContextName));
+ 
+-        if ((service->requestedPresentationContext = LST_Create()) == NULL) return EC_MemoryExhausted;
++        if ((service->requestedPresentationContext = LST_Create()) == NULL)
++        {
++            /* free memory allocated by parseAssociate() before returning */
++            destroyAssociatePDUPresentationContextList(&assoc.presentationContextList);
++            destroyUserInformationLists(&assoc.userInfo);
++            return EC_MemoryExhausted;
++        }
+         if (translatePresentationContextList(&assoc.presentationContextList,
+                                              &assoc.userInfo.SCUSCPRoleList,
+                                              &service->requestedPresentationContext).bad())
+         {
++            /* free memory allocated by parseAssociate() before returning */
++            destroyAssociatePDUPresentationContextList(&assoc.presentationContextList);
++            destroyUserInformationLists(&assoc.userInfo);
+             return DUL_PCTRANSLATIONFAILURE;
+         }
+ 
+@@ -1252,14 +1291,26 @@
+          */
+         if (assoc.userInfo.extNegList != NULL) {
+             service->requestedExtNegList = new SOPClassExtendedNegotiationSubItemList;
+-            if (service->requestedExtNegList == NULL) return EC_MemoryExhausted;
++            if (service->requestedExtNegList == NULL)
++            {
++                /* free memory allocated by parseAssociate() before returning */
++                destroyAssociatePDUPresentationContextList(&assoc.presentationContextList);
++                destroyUserInformationLists(&assoc.userInfo);
++                return EC_MemoryExhausted;
++            }
+             appendList(*assoc.userInfo.extNegList, *service->requestedExtNegList);
+         }
+ 
+         /* user identity negotiation: Remember request values in association parameters (copy)*/
+         if (assoc.userInfo.usrIdent != NULL) {
+           service->reqUserIdentNeg = new UserIdentityNegotiationSubItemRQ();
+-          if (service->reqUserIdentNeg == NULL) return EC_MemoryExhausted;
++          if (service->reqUserIdentNeg == NULL)
++          {
++              /* free memory allocated by parseAssociate() before returning */
++              destroyAssociatePDUPresentationContextList(&assoc.presentationContextList);
++              destroyUserInformationLists(&assoc.userInfo);
++              return EC_MemoryExhausted;
++          }
+             *(service->reqUserIdentNeg) = *(OFstatic_cast(UserIdentityNegotiationSubItemRQ*,assoc.userInfo.usrIdent));
+         }
+ 
+@@ -3947,6 +3998,8 @@
+         {
+             char buf1[256];
+             OFStandard::snprintf(buf1, sizeof(buf1), "DUL Peer supplied illegal number of transfer syntaxes (%d)", 0);
++            /* free the (empty) transfer syntax list allocated for this context above */
++            LST_Destroy(&userContext->proposedTransferSyntax);
+             free(userContext);
+             return makeDcmnetCondition(DULC_PEERILLEGALXFERSYNTAXCOUNT, OF_error, buf1);
+         }
+--- dcmtk.orig/dcmnet/tests/CMakeLists.txt
++++ dcmtk/dcmnet/tests/CMakeLists.txt
+@@ -1,5 +1,6 @@
+ # declare executables
+ DCMTK_ADD_TEST_EXECUTABLE(dcmnet_tests
++  tassocleak.cc
+   tdimse.cc
+   tdump.cc
+   tests.cc
+--- dcmtk.orig/dcmnet/tests/Makefile.in
++++ dcmtk/dcmnet/tests/Makefile.in
+@@ -25,7 +25,7 @@
+ LOCALLIBS = -ldcmnet -ldcmdata -loflog -lofstd -loficonv $(ZLIBLIBS) \
+ 	$(TCPWRAPPERLIBS) $(CHARCONVLIBS) $(MATHLIBS)
+ 
+-objs = tests.o tdump.o tdimse.o tpool.o tscuscp.o tscusession.o
++objs = tests.o tdump.o tdimse.o tassocleak.o tpool.o tscuscp.o tscusession.o
+ progs = tests
+ 
+ 
+--- /dev/null
++++ dcmtk/dcmnet/tests/tassocleak.cc
+@@ -0,0 +1,312 @@
++/*
++ *
++ *  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:  dcmnet
++ *
++ *  Author:  Michael Onken
++ *
++ *  Purpose: Integration regression tests that drive an in-process DcmSCP with
++ *           hand-crafted A-ASSOCIATE-RQ PDUs. They exercise the receiver-side
++ *           association request handling on malformed input that a conformant
++ *           DCMTK requestor (DcmSCU / the ASC_* API) would refuse to put on
++ *           the wire. Built with DCMTK_WITH_SANITIZERS=ON these tests double
++ *           as memory-error / leak regression tests for the DUL association
++ *           parsing and translation code paths.
++ *
++ */
++
++
++#include "dcmtk/config/osconfig.h"    /* make sure OS specific configuration is included first */
++
++#ifdef WITH_THREADS
++
++#include "dcmtk/ofstd/oftest.h"
++#include "dcmtk/ofstd/ofstd.h"        /* for OFStandard::initializeNetwork() */
++#include "dcmtk/ofstd/ofthread.h"     /* for OFThread */
++#include "dcmtk/dcmnet/scp.h"
++#include "dcmtk/dcmnet/dcmtrans.h"    /* for DcmTCPConnection */
++#include "dcmtk/dcmnet/dcompat.h"     /* for platform socket headers */
++#include "dcmtk/dcmnet/dulstruc.h"    /* DUL_TYPE* constants, DUL_PROTOCOL */
++#include "dcmtk/dcmnet/dul.h"
++
++
++/* --------------------------------------------------------------------------
++ *  Minimal in-process SCP that accepts exactly one association attempt and
++ *  then returns. The DUL state machine (and therefore the association request
++ *  parsing/translation) runs inside ASC_receiveAssociation() before the SCP
++ *  gets a chance to accept or reject, so it suffices to let the SCP try to
++ *  receive a single association.
++ * ------------------------------------------------------------------------ */
++struct OneShotReceiverSCP : DcmSCP, OFThread
++{
++    OneShotReceiverSCP()
++        : DcmSCP()
++        , m_listen_result(EC_NotYetImplemented)
++        , m_portNum(0)
++    {
++        DcmSCPConfig& config = getConfig();
++        config.setAETitle("RECV_SCP");
++        // Non-blocking accept with a short connection timeout, so the accept
++        // loop cannot get stuck if a (future) malformed PDU leaves the SCP
++        // waiting on a partial read, or if no client connects at all. For the
++        // normal flow the client connects immediately, so this timeout is not
++        // hit and adds no delay.
++        config.setConnectionBlockingMode(DUL_NOBLOCK);
++        config.setConnectionTimeout(2 /* seconds */);
++        config.setHostLookupEnabled(OFFalse);
++        config.setPort(0); // OS chooses a free port
++        OFList<OFString> xfers;
++        xfers.push_back(UID_LittleEndianImplicitTransferSyntax);
++        OFCHECK(config.addPresentationContext(UID_VerificationSOPClass, xfers).good());
++        OFCHECK(openListenPort().good());
++        m_portNum = config.getPort();
++    }
++
++    /** Stop the accept loop after the single association attempt has been
++     *  handled, so the SCP thread returns and can be joined.
++     */
++    virtual OFBool stopAfterCurrentAssociation()
++    {
++        return OFTrue;
++    }
++
++    /** Also stop if the connection timeout elapses without any association
++     *  request. Together with the non-blocking mode above this guarantees the
++     *  accept loop terminates even if no client ever connects, so a stuck test
++     *  fails fast (via join()) instead of hanging.
++     */
++    virtual OFBool stopAfterConnectionTimeout()
++    {
++        return OFTrue;
++    }
++
++    virtual void run()
++    {
++        m_listen_result = acceptAssociations();
++    }
++
++    OFCondition m_listen_result;
++    Uint16 m_portNum;
++};
++
++
++/* Write a 16-bit big-endian value into buf and advance the cursor. */
++static void put_u16_be(unsigned char *&p, unsigned short v)
++{
++    *p++ = OFstatic_cast(unsigned char, (v >> 8) & 0xff);
++    *p++ = OFstatic_cast(unsigned char, v & 0xff);
++}
++
++/* Write a 32-bit big-endian value into buf and advance the cursor. */
++static void put_u32_be(unsigned char *&p, unsigned long v)
++{
++    *p++ = OFstatic_cast(unsigned char, (v >> 24) & 0xff);
++    *p++ = OFstatic_cast(unsigned char, (v >> 16) & 0xff);
++    *p++ = OFstatic_cast(unsigned char, (v >> 8) & 0xff);
++    *p++ = OFstatic_cast(unsigned char, v & 0xff);
++}
++
++/* Write a sub-item header (type + reserved + 2-byte big-endian length) and
++ * advance the cursor. */
++static void put_subitem_header(unsigned char *&p, unsigned char type, unsigned short bodyLen)
++{
++    *p++ = type;
++    *p++ = 0x00;
++    put_u16_be(p, bodyLen);
++}
++
++
++/* Open a blocking TCP connection to 127.0.0.1:port. Sending and
++ * tearing the connection down is done via DcmTCPConnection so the test needs
++ * no platform-specific socket code. Returns an invalid socket on failure. */
++static DcmNativeSocketType connect_loopback(Uint16 port)
++{
++    DcmNativeSocketType sock = socket(AF_INET, SOCK_STREAM, 0);
++    if (sock == OFstatic_cast(DcmNativeSocketType, -1))
++        return sock;
++
++    struct sockaddr_in addr;
++    memset(&addr, 0, sizeof(addr));
++    addr.sin_family = AF_INET;
++    addr.sin_port = htons(port);
++    addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
++
++    if (connect(sock, OFreinterpret_cast(struct sockaddr*, &addr), sizeof(addr)) != 0)
++        return OFstatic_cast(DcmNativeSocketType, -1);
++    return sock;
++}
++
++
++/* Build an A-ASSOCIATE-RQ PDU whose single presentation context contains an
++ * Abstract Syntax sub-item (Verification SOP Class) but NO Transfer Syntax
++ * sub-items. The PDU is otherwise well-formed: Application Context item +
++ * User Information item with Maximum Length and Implementation Class UID
++ * sub-items.
++ *
++ * We cannot use the existing C++/C API to construct this since in DCMTK
++ * it is protected not to run any Presentation Contexts with 0 transfer
++ * syntaxes.
++ *
++ * @param  outLen  receives the total PDU length on the wire (incl. preamble)
++ * @return heap-allocated buffer (caller frees with delete[])
++ */
++static unsigned char* build_zero_ts_assoc_rq(unsigned long &outLen)
++{
++    const char *appCtx       = "1.2.840.10008.3.1.1.1";  // DICOM Application Context
++    const char *abstractStx  = "1.2.840.10008.1.1";      // Verification SOP Class
++    const char *implClassUID = "1.2.276.0.7230010.3.0.3.6.0"; // random digits
++
++    const unsigned short appCtxLen   = OFstatic_cast(unsigned short, strlen(appCtx));
++    const unsigned short abstractLen = OFstatic_cast(unsigned short, strlen(abstractStx));
++    const unsigned short implLen     = OFstatic_cast(unsigned short, strlen(implClassUID));
++
++    // Sub-PDU sizes (4-byte header + body).
++    const unsigned long appCtxItem    = 4 + appCtxLen;
++    const unsigned long abstractItem  = 4 + abstractLen;
++    // Presentation Context body = ctxID + 3 reserved + abstract syntax item.
++    const unsigned long pcBody        = 4 + abstractItem;
++    const unsigned long pcItem        = 4 + pcBody;
++    const unsigned long maxLenItem    = 4 + 4;            // Max Length sub-item (body = 4-byte maxPDU)
++    const unsigned long implItem      = 4 + implLen;      // Implementation Class UID sub-item
++    const unsigned long userInfoBody  = maxLenItem + implItem;
++    const unsigned long userInfoItem  = 4 + userInfoBody;
++
++    // Fixed A-ASSOCIATE-RQ header before variable items:
++    //   protocol(2) + reserved(2) + calledAE(16) + callingAE(16) + reserved(32) = 68
++    const unsigned long fixedAfterLen = 2 + 2 + 16 + 16 + 32;
++    const unsigned long variableItems = appCtxItem + pcItem + userInfoItem;
++
++    // PDU length field counts everything AFTER the 6-byte preamble
++    // (type + reserved + 4-byte length), i.e. the fixed header plus items.
++    const unsigned long pduPayloadLen = fixedAfterLen + variableItems;
++    const unsigned long totalLen      = 6 + pduPayloadLen;
++
++    unsigned char *buf = new unsigned char[totalLen];
++    unsigned char *p = buf;
++
++    // PDU preamble.
++    *p++ = DUL_TYPEASSOCIATERQ;  // 0x01
++    *p++ = 0x00;                 // reserved
++    put_u32_be(p, pduPayloadLen);
++
++    // Fixed header.
++    put_u16_be(p, DUL_PROTOCOL); // protocol version
++    *p++ = 0x00; *p++ = 0x00;    // reserved
++    {
++        const char *calledAE  = "RECV_SCP        "; // 16 bytes, space padded
++        const char *callingAE = "SEND_SCU        "; // 16 bytes, space padded
++        for (int i = 0; i < 16; ++i) *p++ = OFstatic_cast(unsigned char, calledAE[i]);
++        for (int i = 0; i < 16; ++i) *p++ = OFstatic_cast(unsigned char, callingAE[i]);
++    }
++    for (int i = 0; i < 32; ++i) *p++ = 0x00; // reserved
++
++    // Application Context item.
++    put_subitem_header(p, DUL_TYPEAPPLICATIONCONTEXT, appCtxLen);
++    for (unsigned short i = 0; i < appCtxLen; ++i) *p++ = OFstatic_cast(unsigned char, appCtx[i]);
++
++    // Presentation Context item (with abstract syntax, NO transfer syntax).
++    put_subitem_header(p, DUL_TYPEPRESENTATIONCONTEXTRQ, OFstatic_cast(unsigned short, pcBody));
++    *p++ = 0x01;  // presentation context ID (odd, as required)
++    *p++ = 0x00;  // reserved
++    *p++ = 0x00;  // reserved (result/reason, unused in RQ)
++    *p++ = 0x00;  // reserved
++    //   Abstract Syntax sub-item.
++    put_subitem_header(p, DUL_TYPEABSTRACTSYNTAX, abstractLen);
++    for (unsigned short i = 0; i < abstractLen; ++i) *p++ = OFstatic_cast(unsigned char, abstractStx[i]);
++    //   (intentionally no Transfer Syntax sub-item)
++
++    // User Information item.
++    put_subitem_header(p, DUL_TYPEUSERINFO, OFstatic_cast(unsigned short, userInfoBody));
++    //   Maximum Length sub-item (body = 4-byte maximum PDU length).
++    put_subitem_header(p, DUL_TYPEMAXLENGTH, 4);
++    put_u32_be(p, 16384);
++    //   Implementation Class UID sub-item.
++    put_subitem_header(p, DUL_TYPEIMPLEMENTATIONCLASSUID, implLen);
++    for (unsigned short i = 0; i < implLen; ++i) *p++ = OFstatic_cast(unsigned char, implClassUID[i]);
++
++    OFCHECK_EQUAL(OFstatic_cast(unsigned long, p - buf), totalLen);
++    outLen = totalLen;
++    return buf;
++}
++
++
++/* Regression test for two leaks on the receiver-side handling of an
++ * A-ASSOCIATE-RQ whose single presentation context carries an Abstract Syntax
++ * but ZERO Transfer Syntaxes:
++ *
++ *   1. parseAssociate() succeeds, then translatePresentationContextList()
++ *      rejects the context with an illegal transfer-syntax count and
++ *      AE_6_ExamineAssociateRequest() returns DUL_PCTRANSLATIONFAILURE. The
++ *      already-parsed PRV_ASSOCIATEPDU (presentation context + user
++ *      information lists) must be freed on that error path.
++ *   2. translatePresentationContextList() itself must free the (empty)
++ *      proposedTransferSyntax list it allocated for the rejected context.
++ *
++ * Both leaks are tracked in DCMTK issue #1217.
++ *
++ * A conformant DCMTK requestor cannot emit a zero-transfer-syntax context
++ * (ASC_addPresentationContext rejects it), so the malicious PDU is crafted by
++ * hand and sent over a raw TCP connection to an in-process DcmSCP. The leaks
++ * are asserted implicitly by LeakSanitizer at process exit (build with
++ * DCMTK_WITH_SANITIZERS=ON on Linux); the OFCHECKs below only assert the
++ * harness behaved (socket connected, send succeeded, SCP thread joined).
++ */
++OFTEST(dcmnet_scp_assocRQ_zeroTransferSyntax_no_leak)
++{
++    OFStandard::initializeNetwork();
++
++    OneShotReceiverSCP scp;
++    // The listen socket is already bound and listening (openListenPort() runs
++    // in the SCP constructor), so a client connect() succeeds via the TCP
++    // backlog even before the accept thread is scheduled -- no startup sleep
++    // is needed here.
++    scp.start();
++
++    DcmNativeSocketType sock = connect_loopback(scp.m_portNum);
++    OFCHECK(sock != OFstatic_cast(DcmNativeSocketType, -1));
++
++    if (sock != OFstatic_cast(DcmNativeSocketType, -1))
++    {
++        DcmTCPConnection conn(sock); // takes ownership of the socket
++
++        unsigned long pduLen = 0;
++        unsigned char *pdu = build_zero_ts_assoc_rq(pduLen);
++
++        // Send the hand-built A-ASSOCIATE-RQ. This drives the SCP through
++        // parseAssociate() + translatePresentationContextList() (the original leak path).
++        unsigned long sent = 0;
++        OFBool sendOk = OFTrue;
++        while (sent < pduLen)
++        {
++            ssize_t n = conn.write(pdu + sent, OFstatic_cast(size_t, pduLen - sent));
++            if (n <= 0) { sendOk = OFFalse; break; }
++            sent += OFstatic_cast(unsigned long, n);
++        }
++        OFCHECK(sendOk);
++
++        // Close immediately after sending instead of reading a response. The
++        // buffered PDU is still delivered to the SCP before the FIN, so it
++        // reaches the leak path; the FIN then unblocks the SCP's receive call
++        // at once. Reading here would instead leave both ends blocked on each
++        // other until the socket receive timeout (dcmSocketReceiveTimeout,
++        // 60s by default) expired -- which made this test take ~63s.
++        delete[] pdu;
++        conn.close();
++    }
++
++    // Wait for the SCP thread to finish handling the (failed) association.
++    const int joinResult = scp.join();
++    OFCHECK(joinResult != OFThread::busy);
++}
++
++#endif // WITH_THREADS
+--- dcmtk.orig/dcmnet/tests/tests.cc
++++ dcmtk/dcmnet/tests/tests.cc
+@@ -51,6 +51,8 @@
+ 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_assocRQ_zeroTransferSyntax_no_leak);
++
+ #endif // WITH_THREADS
+ 
+ OFTEST_MAIN("dcmnet")


=====================================
debian/patches/0023-CVE-2026-52868.patch
=====================================
@@ -0,0 +1,746 @@
+commit e3878daf870cd2db50eadfde38615f0afae8a584
+Author: Michael Onken <onken at open-connections.de>
+Date:   Tue May 19 17:16:08 2026 +0200
+
+    Fix path traversal in wlmscpfs through Called AET.
+    
+    The wlmscpfs SCP appended the Called Application Entity Title received
+    in the A-ASSOCIATE-RQ directly onto the configured worklist data file
+    path and used the existence of the resulting directory as an access
+    control decision. Because DICOM VR AE permits the characters "/", "\"
+    and ".", a peer could send a 16-byte AE title such as "../secret/VICTIM"
+    and have wlmscpfs serve worklist records from a sibling directory of
+    the configured root. With option --request-file-path enabled, the same
+    AE title (and the Patient ID) was substituted into the output filename
+    template without sanitization, producing an arbitrary-location write
+    primitive outside the configured request file directory.
+    
+    This commit closes both holes:
+    
+    - wlmscpfs now rejects any A-ASSOCIATE-RQ whose Called AE title is not
+      safe to use as a single filesystem path component, refusing the
+      association with WLM_BAD_AE_SERVICE. The validation is implemented in
+      the new static method WlmFileSystemInteractionManager::IsValidAETitle\
+      ForFilesystem, which rejects empty or over-long titles, any title
+      containing a dot, and delegates the remaining character check to
+      OFStandard::sanitizeAETitle (sanitize-and-compare).
+    
+    - The placeholder substitution in storeRequestToFile now passes each
+      substituted value (#a, #c, #p) through OFStandard::sanitizeAETitle
+      before insertion, and additionally sanitizes the final filename, so
+      that any path separator surviving the substitution is defanged.
+    
+    Supporting changes:
+    
+    - Promote storescp's private sanitizeAETitle helper into the public
+      OFStandard::sanitizeAETitle, with documentation noting that the
+      function is also used by wlmscpfs to validate filesystem path
+      components (so widening the allow list has downstream effects).
+    - Replace storescp's local copy of the helper with the new public one.
+    - Add a forward declaration of DcmSequenceOfItems in wlfsim.h that
+      was previously missing (existing callers happened to include dctk.h
+      first).
+    - Document the new behaviour in the wlmscpfs man page.
+    
+    Tests:
+    
+    - New ofstd_OFStandard_sanitizeAETitle test covers the lifted helper
+      (path separators, NUL, control bytes, high-range bytes, shell
+      metacharacters, the quotation-mark preservation behaviour, and
+      empty/length-1 edge cases).
+    - New dcmwlm_aetitle_validation test covers the validator directly
+      with every path-traversal payload from the bug report ("../secret/\
+      VICTIM", "../CARDIOLOGY", "..", ".", "..\secret"), dotted variants
+      ("MY.AE", "foo..bar", ".foo", "foo."), structural rejections (empty,
+      17 bytes, embedded NUL, tab, 0xFF), shell metacharacters, and a row
+      of legitimate AE titles that must still be accepted.
+    - dcmwlm previously had no OFTEST scaffolding; tests.cc has been added
+      along with the corresponding CMakeLists.txt and Makefile.in entries.
+    
+    Thanks to Abhinav Agarwal for the report.
+
+--- dcmtk.orig/dcmnet/apps/storescp.cc
++++ dcmtk/dcmnet/apps/storescp.cc
+@@ -98,7 +98,6 @@
+ static OFString replaceChars( const OFString &srcstr, const OFString &pattern, const OFString &substitute );
+ static void executeCommand( const OFString &cmd );
+ static void cleanChildren(pid_t pid, OFBool synch);
+-static void sanitizeAETitle(OFString& aet);
+ static OFCondition acceptUnknownContextsWithPreferredTransferSyntaxes(
+          T_ASC_Parameters * params,
+          const char* transferSyntaxes[],
+@@ -2230,34 +2229,6 @@
+   lastStudySubdirectoryPathAndName.clear();
+ }
+ 
+-/* replace all characters that might be interpreted by the shell with underscores
+- */
+-static void sanitizeAETitle(OFString& aet)
+-{
+-  static const char sanitized_aetitle_charset[] =
+-  {
+-    ' ', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '-', '.', '_',
+-    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', '_', '_', '_', '_', '_',
+-    '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
+-    'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '_', '_', '_', '_', '_',
+-    '_', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
+-    'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '_', '_', '_', '_', '_'
+-  };
+-
+-  // the aet string starts and ends with quotation marks. We ignore these.
+-  size_t len = aet.length();
+-  if (len < 3) return;
+-
+-  char c;
+-  --len;
+-  for (size_t i=1; i < len; ++i)
+-  {
+-    c = aet[i];
+-    if (c != 0 && (c < 32 || c >= 127)) c = '_'; else c = sanitized_aetitle_charset[c-32];
+-    aet[i] = c;
+-  }
+-}
+-
+ static void executeOnReception()
+     /*
+      * This function deals with the execution of the command line which was passed
+@@ -2293,7 +2264,7 @@
+   // perform substitution for placeholder #a.
+   // Note that this string is already enclosed in double quotes at this point
+   s = callingAETitle;
+-  sanitizeAETitle(s);
++  OFStandard::sanitizeAETitle(s);
+   if (s != callingAETitle)
+   {
+     OFLOG_WARN(storescpLogger, "Sanitized unusual characters in calling aetitle, converted from " << callingAETitle << " to " << s << ".");
+@@ -2303,7 +2274,7 @@
+   // perform substitution for placeholder #c.
+   // Note that this string is already enclosed in double quotes at this point
+   s = calledAETitle;
+-  sanitizeAETitle(s);
++  OFStandard::sanitizeAETitle(s);
+   if (s != calledAETitle)
+   {
+     OFLOG_WARN(storescpLogger, "Sanitized unusual characters in called aetitle, converted from " << calledAETitle << " to " << s << ".");
+@@ -2313,7 +2284,7 @@
+   // perform substitution for placeholder #r.
+   // Note that this string is already enclosed in double quotes at this point
+   s = callingPresentationAddress;
+-  sanitizeAETitle(s);
++  OFStandard::sanitizeAETitle(s);
+   if (s != callingPresentationAddress)
+   {
+     OFLOG_WARN(storescpLogger, "Sanitized unusual characters in calling presentation address, converted from " << callingPresentationAddress << " to " << s << ".");
+@@ -2429,7 +2400,7 @@
+   // perform substitution for placeholder #a.
+   // Note that this string is already enclosed in double quotes at this point
+   s = callingAETitle;
+-  sanitizeAETitle(s);
++  OFStandard::sanitizeAETitle(s);
+   if (s != callingAETitle)
+   {
+     OFLOG_WARN(storescpLogger, "Sanitized unusual characters in calling aetitle, converted from " << callingAETitle << " to " << s << ".");
+@@ -2439,7 +2410,7 @@
+   // perform substitution for placeholder #c.
+   // Note that this string is already enclosed in double quotes at this point
+   s = calledAETitle;
+-  sanitizeAETitle(s);
++  OFStandard::sanitizeAETitle(s);
+   if (s != calledAETitle)
+   {
+     OFLOG_WARN(storescpLogger, "Sanitized unusual characters in called aetitle, converted from " << calledAETitle << " to " << s << ".");
+@@ -2449,7 +2420,7 @@
+   // perform substitution for placeholder #r.
+   // Note that this string is already enclosed in double quotes at this point
+   s = callingPresentationAddress;
+-  sanitizeAETitle(s);
++  OFStandard::sanitizeAETitle(s);
+   if (s != callingPresentationAddress)
+   {
+     OFLOG_WARN(storescpLogger, "Sanitized unusual characters in calling presentation address, converted from " << callingPresentationAddress << " to " << s << ".");
+--- dcmtk.orig/dcmwlm/docs/wlmscpfs.man
++++ dcmtk/dcmwlm/docs/wlmscpfs.man
+@@ -231,6 +231,23 @@
+ Table K.6-1 in part 4 annex K of the DICOM standard lists all corresponding
+ type 1 attributes (see column "Return Key Type").
+ 
++The called Application Entity Title from an incoming A-ASSOCIATE-RQ is used
++as a subdirectory name below the \e --data-file-path directory in order to
++look up the worklist files that this association may access.  The DICOM VR
++AE (see DICOM PS3.5 Section 6.1.3) permits the characters "/" (forward slash)
++and "." (full stop), so a conformant peer can send an AE title such as
++"../OTHER" that would resolve to a sibling directory of the configured
++worklist root.  To prevent this, \b wlmscpfs rejects any A-ASSOCIATE-RQ
++whose called AE title is not safe to use as a single filesystem path
++component.  The set of accepted characters is letters, digits, space and
++the characters "-", ":", "@" and "_"; any other byte (path separators
++"/" and "\\", NUL, control bytes, bytes outside the printable ASCII range,
++shell metacharacters as well as the dot character) causes the association
++to be rejected.  Dots are rejected outright (rather than only the
++standalone components "." and "..") because real-world AE titles never
++contain dots and rejecting the entire character avoids any need to reason
++about platform-specific path normalization corner cases.
++
+ \subsection wlmscpfs_request_files Writing Request Files
+ 
+ Providing option \e --request-file-path enables writing of the incoming C-FIND
+@@ -289,6 +306,13 @@
+ cannot be written successfully or will look broken once written.  Also, an empty
+ Patient ID is used as such, i.e. the \#p will be replaced with an empty string.
+ 
++For security reasons, the values substituted for the placeholders \#a, \#c and
++\#p are sanitized before they are inserted into the resulting file name: path
++separators ("/", "\\") and other characters that are not safe in a file name
++component are replaced with the underscore character ("_").  This prevents
++attacker-controlled values transmitted over the network (called/calling AE
++title, Patient ID) from escaping the configured request file directory.
++
+ \subsection wlmscpfs_dicom_conformance DICOM Conformance
+ 
+ The \b wlmscpfs application supports the following SOP Classes as an SCP:
+--- dcmtk.orig/dcmwlm/include/dcmtk/dcmwlm/wlfsim.h
++++ dcmtk/dcmwlm/include/dcmtk/dcmwlm/wlfsim.h
+@@ -34,6 +34,7 @@
+ class DcmTagKey;
+ class OFCondition;
+ class DcmItem;
++class DcmSequenceOfItems;
+ class OFdirectory_iterator;
+ 
+ /** This class encapsulates data structures and operations for managing
+@@ -199,13 +200,53 @@
+ 
+       /** Checks if the given called application entity title is supported. If this is the case,
+        *  OFTrue will be returned, else OFFalse.
++       *
++       *  The check is performed in two stages. First, the AE title is validated against the
++       *  rules of IsValidAETitleForFilesystem(): titles that contain path separators, embedded
++       *  NUL or other control characters, or any dot character are rejected outright (returning
++       *  OFFalse). This prevents the wire-side AE title from escaping the worklist data file
++       *  path through path traversal. Second, the AE title is appended to the configured
++       *  worklist root path and the resulting directory must exist.
++       *
+        *  @param calledApplicationEntityTitlev The application entity title which shall be checked
+-       *                                       for support. Valid pointer expected.
++       *                                       for support.
+        *  @return OFTrue, if the called application entity title is supported,
+-       *          OFFalse, if the called application entity title is not supported or it is not given.
++       *          OFFalse, if the called application entity title is not supported, not given,
++       *          or rejected because it would not be safe to use as a filesystem path component.
+        */
+     OFBool IsCalledApplicationEntityTitleSupported( const OFString& calledApplicationEntityTitlev );
+ 
++      /** Determines whether the given DICOM Application Entity Title is safe to use as a single
++       *  directory or filename component below the worklist data file path.
++       *
++       *  An AE title is considered safe if and only if all of the following hold:
++       *  - it is not empty and not longer than 16 bytes (the DICOM AE VR length limit),
++       *  - it contains no dot ('.') at all,
++       *  - OFStandard::sanitizeAETitle() would not change any byte of it.
++       *
++       *  The third rule delegates the character-set check to OFStandard::sanitizeAETitle(),
++       *  which is the same allow list used by \b storescp for filename and shell-substitution
++       *  contexts: letters, digits, space and the characters '-', '.', ':', '@', '_' are kept,
++       *  every other byte (path separators '/' and '\\', NUL, control characters, bytes
++       *  outside the printable ASCII range and shell metacharacters) is replaced. If the
++       *  sanitized string differs from the input, the AE title contains at least one such
++       *  unsafe byte and is rejected.
++       *
++       *  Dots are rejected by the explicit second rule even though sanitizeAETitle() keeps
++       *  them. Real-world AE titles never contain dots, and rejecting the entire character
++       *  avoids any need to reason about path normalization corner cases (".", "..", trailing
++       *  dots on Windows etc.).
++       *
++       *  Note that this delegation couples the rule applied here to the allow list inside
++       *  OFStandard::sanitizeAETitle(). If that allow list is widened in the future, the set
++       *  of AE titles accepted as safe filesystem path components widens too, so any change
++       *  to it must consider downstream effects on \b wlmscpfs.
++       *
++       *  @param aeTitle Application Entity Title to validate.
++       *  @return OFTrue if the AE title is safe to use as a path component, OFFalse otherwise.
++       */
++    static OFBool IsValidAETitleForFilesystem( const OFString& aeTitle );
++
+       /** This function determines the records from the Worklist files that match
+        *  the given search mask and returns the number of matching records. Also,
+        *  this function will store the matching records inside the member variable
+--- dcmtk.orig/dcmwlm/libsrc/wlfsim.cc
++++ dcmtk/dcmwlm/libsrc/wlfsim.cc
+@@ -1,6 +1,6 @@
+ /*
+  *
+- *  Copyright (C) 1996-2024, OFFIS e.V.
++ *  Copyright (C) 1996-2026, OFFIS e.V.
+  *  All rights reserved.  See COPYRIGHT file for details.
+  *
+  *  This software and supporting documentation were developed by
+@@ -155,6 +155,32 @@
+ 
+ // ----------------------------------------------------------------------------
+ 
++OFBool WlmFileSystemInteractionManager::IsValidAETitleForFilesystem( const OFString& aeTitle )
++{
++  // Length must be 1..16 (DICOM AE VR limit).
++  if( aeTitle.empty() || aeTitle.length() > 16 )
++    return OFFalse;
++
++  // OFStandard::sanitizeAETitle() keeps '.' in its allow list (the function
++  // is also used in shell-substitution contexts where dots are common in
++  // AE titles), so reject dots explicitly here. Rejecting the entire
++  // character avoids any need to reason about platform-specific path
++  // normalization corner cases (".", "..", trailing dots on Windows etc.).
++  if( aeTitle.find('.') != OFString_npos )
++    return OFFalse;
++
++  // Delegate the remaining character check to OFStandard::sanitizeAETitle().
++  // If the sanitizer would change any byte, the AE title contains characters
++  // that are not safe to use as a single filesystem path component (path
++  // separators, NUL, control bytes, bytes outside the printable ASCII range
++  // and shell metacharacters).
++  OFString sanitized = aeTitle;
++  OFStandard::sanitizeAETitle( sanitized );
++  return ( sanitized == aeTitle ) ? OFTrue : OFFalse;
++}
++
++// ----------------------------------------------------------------------------
++
+ OFBool WlmFileSystemInteractionManager::IsCalledApplicationEntityTitleSupported( const OFString& calledApplicationEntityTitlev )
+ // Date         : July 11, 2002
+ // Author       : Thomas Wilkens
+@@ -168,6 +194,15 @@
+   // copy value
+   calledApplicationEntityTitle = calledApplicationEntityTitlev;
+ 
++  // Reject AE titles that are not safe to use as a filesystem path component.
++  // This guards against path traversal via path separators or "../" segments
++  // smuggled in through the wire-side AE title.
++  if( !IsValidAETitleForFilesystem( calledApplicationEntityTitle ) )
++  {
++    DCMWLM_WARN( "Refusing called AE title because it is not safe to use as a directory name (contains path separators, control characters or is \".\"/\"..\"); rejecting association" );
++    return( OFFalse );
++  }
++
+   // Determine complete path to the files that make up the data source.
+   OFString fullPath( dfPath );
+   if( !fullPath.empty() && fullPath[fullPath.length()-1] != PATH_SEPARATOR )
+--- dcmtk.orig/dcmwlm/libsrc/wlmactmg.cc
++++ dcmtk/dcmwlm/libsrc/wlmactmg.cc
+@@ -1174,10 +1174,19 @@
+ static void storeRequestToFile(DcmDataset& request, const OFString& callingAE, const OFString& calledAE, const OFString& reqFilePath, const OFString& reqFileFormat)
+ {
+   OFString fileName = reqFileFormat;
+-  // Called Application Entity Title
+-  OFStringUtil::replace_all(fileName, WLM_CALLED_AETITLE_PLACEHOLDER, calledAE);
+-  // Calling Application Entity Title
+-  OFStringUtil::replace_all(fileName, WLM_CALLING_AETITLE_PLACEHOLDER, callingAE);
++
++  // Each substituted value is sanitized before being inserted into the
++  // filename template. This prevents the calling/called AE title and the
++  // Patient ID (all originating on the wire) from injecting path
++  // separators or other unsafe characters into the resulting filename
++  // and thereby escaping the configured request file directory.
++  OFString safeCalledAE = calledAE;
++  OFStandard::sanitizeAETitle(safeCalledAE);
++  OFStringUtil::replace_all(fileName, WLM_CALLED_AETITLE_PLACEHOLDER, safeCalledAE);
++
++  OFString safeCallingAE = callingAE;
++  OFStandard::sanitizeAETitle(safeCallingAE);
++  OFStringUtil::replace_all(fileName, WLM_CALLING_AETITLE_PLACEHOLDER, safeCallingAE);
+ 
+   // Process ID
+   int processID = dcmtk::log4cplus::internal::get_process_id();
+@@ -1199,8 +1208,15 @@
+   // Patient ID goes last since it might contain placeholders again (".#x...)"
+   OFString patientID;
+   request.findAndGetOFStringArray(DCM_PatientID, patientID);
++  OFStandard::sanitizeAETitle(patientID);
+   OFStringUtil::replace_all(fileName, WLM_PATIENT_ID_PLACEHOLDER, patientID);
+ 
++  // Defense in depth: also sanitize the fully assembled filename so that
++  // any path separator that survived (for example through interaction
++  // between placeholder substitutions or via the user-supplied format
++  // string itself) is replaced before the file is opened.
++  OFStandard::sanitizeAETitle(fileName);
++
+   // Finally store file
+   STD_NAMESPACE ofstream outputStream;
+   OFString fullPath;
+--- dcmtk.orig/dcmwlm/tests/CMakeLists.txt
++++ dcmtk/dcmwlm/tests/CMakeLists.txt
+@@ -1,5 +1,10 @@
+ # declare executables
+ DCMTK_ADD_TEST_EXECUTABLE(wltest wltest.cc)
++DCMTK_ADD_TEST_EXECUTABLE(dcmwlm_tests tests.cc twlaetval.cc)
+ 
+ # make sure executables are linked to the corresponding libraries
+ DCMTK_TARGET_LINK_MODULES(wltest dcmwlm dcmnet dcmtls dcmdata oflog ofstd)
++DCMTK_TARGET_LINK_MODULES(dcmwlm_tests dcmwlm)
++
++# This macro parses tests.cc and registers all tests
++DCMTK_ADD_TESTS(dcmwlm)
+--- dcmtk.orig/dcmwlm/tests/Makefile.in
++++ dcmtk/dcmwlm/tests/Makefile.in
+@@ -25,19 +25,25 @@
+ LOCALLIBS = -ldcmwlm -ldcmnet -ldcmdata -loflog -lofstd -loficonv $(ZLIBLIBS) \
+ 	$(TCPWRAPPERLIBS) $(CHARCONVLIBS) $(MATHLIBS)
+ 
+-objs = wltest.o
+-progs = wltest
++test_objs = tests.o twlaetval.o
++objs = wltest.o $(test_objs)
++progs = wltest dcmwlm_tests
+ 
+ 
+ all: $(progs)
+ 
+-wltest: $(objs)
+-	$(CXX) $(CXXFLAGS) $(LIBDIRS) $(LDFLAGS) -o $@ $(objs) $(LOCALLIBS) $(LIBS)
++wltest: wltest.o
++	$(CXX) $(CXXFLAGS) $(LIBDIRS) $(LDFLAGS) -o $@ wltest.o $(LOCALLIBS) $(LIBS)
+ 
++dcmwlm_tests: $(test_objs)
++	$(CXX) $(CXXFLAGS) $(LIBDIRS) $(LDFLAGS) -o $@ $(test_objs) $(LOCALLIBS) $(LIBS)
+ 
+-check:
+ 
+-check-exhaustive:
++check: dcmwlm_tests
++	./dcmwlm_tests
++
++check-exhaustive: dcmwlm_tests
++	./dcmwlm_tests -x
+ 
+ 
+ install: all
+--- /dev/null
++++ dcmtk/dcmwlm/tests/tests.cc
+@@ -0,0 +1,27 @@
++/*
++ *
++ *  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:  dcmwlm
++ *
++ *  Author:  Michael Onken
++ *
++ *  Purpose: main test program
++ *
++ */
++
++#include "dcmtk/config/osconfig.h"
++#include "dcmtk/ofstd/oftest.h"
++
++OFTEST_REGISTER(dcmwlm_aetitle_validation);
++
++OFTEST_MAIN("dcmwlm")
+--- /dev/null
++++ dcmtk/dcmwlm/tests/twlaetval.cc
+@@ -0,0 +1,116 @@
++/*
++ *
++ *  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:  dcmwlm
++ *
++ *  Author:  Michael Onken
++ *
++ *  Purpose: Tests for the AE title validation used by the wlmscpfs
++ *           filesystem worklist SCP to guard against path traversal
++ *           through the Called Application Entity Title.
++ *
++ */
++
++#include "dcmtk/config/osconfig.h"
++
++#include "dcmtk/ofstd/oftest.h"
++#include "dcmtk/ofstd/ofstring.h"
++#include "dcmtk/dcmwlm/wlfsim.h"
++
++
++// Helper: build an OFString that contains a single NUL byte in the middle
++// of an otherwise printable AE title. OFString tolerates embedded NUL bytes
++// when constructed from a (pointer, length) pair, which is how a NUL byte
++// could enter wlmscpfs from the wire.
++static OFString aeTitleWithEmbeddedNul()
++{
++    const char raw[] = { 'A', 'B', '\0', 'C', 'D' };
++    return OFString(raw, sizeof(raw));
++}
++
++
++OFTEST(dcmwlm_aetitle_validation)
++{
++    // ----- Path traversal payloads from the original bug report. -----
++    // All of these must be rejected; if any of them is accepted, the
++    // CVE-2026-... path traversal hole is open again.
++
++    // 16-byte payload from the proof of concept ("../secret/VICTIM").
++    OFCHECK(!WlmFileSystemInteractionManager::IsValidAETitleForFilesystem("../secret/VICTIM"));
++
++    // Multi-AET demo ("../CARDIOLOGY").
++    OFCHECK(!WlmFileSystemInteractionManager::IsValidAETitleForFilesystem("../CARDIOLOGY"));
++
++    // Plain parent reference and self reference.
++    OFCHECK(!WlmFileSystemInteractionManager::IsValidAETitleForFilesystem(".."));
++    OFCHECK(!WlmFileSystemInteractionManager::IsValidAETitleForFilesystem("."));
++
++    // Windows-style separator.
++    OFCHECK(!WlmFileSystemInteractionManager::IsValidAETitleForFilesystem("..\\secret"));
++
++    // Leading-dot variants and dots in the middle. None of these are
++    // path-traversal payloads on their own, but we reject any AE title
++    // that contains a dot, see the documentation of
++    // IsValidAETitleForFilesystem() for the rationale.
++    OFCHECK(!WlmFileSystemInteractionManager::IsValidAETitleForFilesystem(".foo"));
++    OFCHECK(!WlmFileSystemInteractionManager::IsValidAETitleForFilesystem("foo."));
++    OFCHECK(!WlmFileSystemInteractionManager::IsValidAETitleForFilesystem("foo..bar"));
++    OFCHECK(!WlmFileSystemInteractionManager::IsValidAETitleForFilesystem("MY.AE"));
++
++    // ----- Other unsafe input. -----
++
++    // Empty AE title.
++    OFCHECK(!WlmFileSystemInteractionManager::IsValidAETitleForFilesystem(""));
++
++    // Too long (17 bytes; the DICOM AE VR allows at most 16).
++    OFCHECK(!WlmFileSystemInteractionManager::IsValidAETitleForFilesystem("ABCDEFGHIJKLMNOPQ"));
++
++    // Embedded NUL byte.
++    OFCHECK(!WlmFileSystemInteractionManager::IsValidAETitleForFilesystem(aeTitleWithEmbeddedNul()));
++
++    // Control character (tab).
++    OFCHECK(!WlmFileSystemInteractionManager::IsValidAETitleForFilesystem("AB\tCD"));
++
++    // High-range byte.
++    {
++        OFString withHigh;
++        withHigh.append("AE", 2);
++        withHigh.append(1, OFstatic_cast(char, 0xFF));
++        OFCHECK(!WlmFileSystemInteractionManager::IsValidAETitleForFilesystem(withHigh));
++    }
++
++    // Shell metacharacters: rejected because
++    // OFStandard::sanitizeAETitle() would replace them.
++    OFCHECK(!WlmFileSystemInteractionManager::IsValidAETitleForFilesystem("AE;rm"));
++    OFCHECK(!WlmFileSystemInteractionManager::IsValidAETitleForFilesystem("AE|cat"));
++    OFCHECK(!WlmFileSystemInteractionManager::IsValidAETitleForFilesystem("AE`id`"));
++    OFCHECK(!WlmFileSystemInteractionManager::IsValidAETitleForFilesystem("AE$VAR"));
++
++    // ----- Legitimate AE titles must still be accepted. -----
++
++    // Plain alphanumeric, mixed case.
++    OFCHECK(WlmFileSystemInteractionManager::IsValidAETitleForFilesystem("WORKLIST"));
++    OFCHECK(WlmFileSystemInteractionManager::IsValidAETitleForFilesystem("CARDIOLOGY"));
++    OFCHECK(WlmFileSystemInteractionManager::IsValidAETitleForFilesystem("MyAE"));
++
++    // Single character (minimum length).
++    OFCHECK(WlmFileSystemInteractionManager::IsValidAETitleForFilesystem("A"));
++
++    // Maximum length (16 bytes).
++    OFCHECK(WlmFileSystemInteractionManager::IsValidAETitleForFilesystem("ABCDEFGHIJKLMNOP"));
++
++    // Conformant punctuation that is also safe as a path component.
++    OFCHECK(WlmFileSystemInteractionManager::IsValidAETitleForFilesystem("MY-AE_1"));
++    OFCHECK(WlmFileSystemInteractionManager::IsValidAETitleForFilesystem("AE at HOST"));
++    OFCHECK(WlmFileSystemInteractionManager::IsValidAETitleForFilesystem("AE:1"));
++}
+--- dcmtk.orig/ofstd/include/dcmtk/ofstd/ofstd.h
++++ dcmtk/ofstd/include/dcmtk/ofstd/ofstd.h
+@@ -1181,6 +1181,44 @@
+      */
+     static void sanitizeFilename(char *fname);
+ 
++    /** sanitize a DICOM Application Entity Title for safe use as a
++     *  substitution value in a filename or in a command line that will
++     *  be passed to a shell.
++     *
++     *  This method maps each byte through an allow list: ASCII letters,
++     *  digits, space and the characters '-', '.', ':', '@', '_' are kept
++     *  unchanged; every other byte (including path separators '/' and
++     *  '\\', NUL, control characters, shell metacharacters and bytes
++     *  outside the printable ASCII range) is replaced by '_'.
++     *
++     *  The aetitle string is expected to be already trimmed (i.e. without
++     *  leading or trailing whitespace). Surrounding quotation marks, if
++     *  present, are preserved unchanged (the first and last byte are not
++     *  rewritten) so that quoted AE titles intended for shell command
++     *  substitution remain quoted.
++     *
++     *  This sanitization is sufficient for preventing shell injection in
++     *  contexts where the AE title is substituted into an already quoted
++     *  command-line argument, and for preventing path separators from
++     *  appearing in filenames. It is NOT sufficient to prevent path
++     *  traversal on its own, because the character '.' is kept and the
++     *  sequence "..", which is a path-relative parent reference on most
++     *  operating systems, is therefore not removed. Callers that
++     *  substitute the result into a filesystem path must reject or
++     *  collapse such sequences explicitly.
++     *
++     *  Note: this function is also used by the wlmscpfs application as
++     *  the basis of its filesystem-path-component validation (an AE title
++     *  is accepted only if sanitizeAETitle() would not change it, in
++     *  addition to an explicit dot rejection). Widening the allow list
++     *  below therefore widens the set of AE titles accepted as worklist
++     *  directory names; any change to it must consider that downstream
++     *  effect.
++     *
++     *  @param aetitle Application Entity Title to be sanitized in place.
++     */
++    static void sanitizeAETitle(OFString& aetitle);
++
+     /** retrieve the name of the default directory for support data.
+      *  On Windows, this method resolves environment variables such as
+      *  \%PROGRAMDATA% in the path, on Posix platforms it just returns
+--- dcmtk.orig/ofstd/libsrc/ofstd.cc
++++ dcmtk/ofstd/libsrc/ofstd.cc
+@@ -3500,6 +3500,42 @@
+ }
+ 
+ 
++// Allow list used by sanitizeAETitle(). Index is (byte - 32), so the
++// table covers the printable ASCII range 0x20..0x7E. Every entry either
++// repeats the input byte (kept) or is '_' (replaced). Kept characters:
++// space, '-', '.', ':', '@', '_' and ASCII letters/digits. All shell
++// metacharacters and path separators map to '_'.
++static const char sanitized_aetitle_charset[] =
++{
++  ' ', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '-', '.', '_',
++  '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', '_', '_', '_', '_', '_',
++  '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
++  'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '_', '_', '_', '_', '_',
++  '_', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
++  'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '_', '_', '_', '_', '_'
++};
++
++
++void OFStandard::sanitizeAETitle(OFString& aetitle)
++{
++    // Preserve a surrounding pair of quotation marks (used by callers
++    // that substitute the AE title into an already quoted shell argument).
++    size_t len = aetitle.length();
++    size_t start = 0;
++    if (len >= 2 && aetitle[0] == '"' && aetitle[len - 1] == '"')
++    {
++        start = 1;
++        --len;
++    }
++    for (size_t i = start; i < len; ++i)
++    {
++        unsigned char c = OFstatic_cast(unsigned char, aetitle[i]);
++        if (c < 32 || c >= 127) aetitle[i] = '_';
++        else aetitle[i] = sanitized_aetitle_charset[c - 32];
++    }
++}
++
++
+ OFString OFStandard::getDefaultSupportDataDir()
+ {
+ #ifdef HAVE_WINDOWS_H
+--- dcmtk.orig/ofstd/tests/tests.cc
++++ dcmtk/ofstd/tests/tests.cc
+@@ -1,6 +1,6 @@
+ /*
+  *
+- *  Copyright (C) 2011-2024, 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
+@@ -48,6 +48,7 @@
+ OFTEST_REGISTER(ofstd_OFStandard_isReadWriteable);
+ OFTEST_REGISTER(ofstd_OFStandard_appendFilenameExtension);
+ OFTEST_REGISTER(ofstd_OFStandard_removeRootDirFromPathname);
++OFTEST_REGISTER(ofstd_OFStandard_sanitizeAETitle);
+ OFTEST_REGISTER(ofstd_OFFile);
+ OFTEST_REGISTER(ofstd_OFString_compare);
+ OFTEST_REGISTER(ofstd_OFString_concatenate);
+--- dcmtk.orig/ofstd/tests/tofstd.cc
++++ dcmtk/ofstd/tests/tofstd.cc
+@@ -1,6 +1,6 @@
+ /*
+  *
+- *  Copyright (C) 2002-2022, OFFIS e.V.
++ *  Copyright (C) 2002-2026, OFFIS e.V.
+  *  All rights reserved.  See COPYRIGHT file for details.
+  *
+  *  This software and supporting documentation were developed by
+@@ -373,6 +373,62 @@
+     OFCHECK(OFStandard::removeRootDirFromPathname(result, nullPtr, nullPtr).good());
+ }
+ 
++OFTEST(ofstd_OFStandard_sanitizeAETitle)
++{
++    // Pure-ASCII alphanumeric is preserved.
++    OFString s = "MY_AE_42";
++    OFStandard::sanitizeAETitle(s);
++    OFCHECK_EQUAL(s, "MY_AE_42");
++
++    // Dots, colons, dashes, '@' are part of the allow list.
++    s = "AE.TITLE:1 at host-1";
++    OFStandard::sanitizeAETitle(s);
++    OFCHECK_EQUAL(s, "AE.TITLE:1 at host-1");
++
++    // Path separators map to underscores.
++    s = "../etc/passwd";
++    OFStandard::sanitizeAETitle(s);
++    OFCHECK_EQUAL(s, ".._etc_passwd");
++
++    // Backslash is replaced (Windows path separator).
++    s = "..\\secret";
++    OFStandard::sanitizeAETitle(s);
++    OFCHECK_EQUAL(s, ".._secret");
++
++    // Shell metacharacters are replaced.
++    s = "AE;rm -rf /";
++    OFStandard::sanitizeAETitle(s);
++    OFCHECK_EQUAL(s, "AE_rm -rf _");
++
++    // Control characters (including NUL inside the string) are replaced.
++    OFString withNul;
++    withNul.append("AB", 2);
++    withNul.append(1, '\0');
++    withNul.append("CD", 2);
++    OFStandard::sanitizeAETitle(withNul);
++    OFCHECK_EQUAL(withNul, OFString("AB_CD"));
++
++    // High-range bytes (>= 0x7F) are replaced.
++    OFString withHigh;
++    withHigh.append("AE", 2);
++    withHigh.append(1, OFstatic_cast(char, 0xFF));
++    OFStandard::sanitizeAETitle(withHigh);
++    OFCHECK_EQUAL(withHigh, OFString("AE_"));
++
++    // A surrounding quotation-mark pair is preserved (storescp behaviour).
++    s = "\"../bad\"";
++    OFStandard::sanitizeAETitle(s);
++    OFCHECK_EQUAL(s, "\".._bad\"");
++
++    // Empty string and length-1 string are handled without crashing.
++    s = "";
++    OFStandard::sanitizeAETitle(s);
++    OFCHECK_EQUAL(s, "");
++    s = "/";
++    OFStandard::sanitizeAETitle(s);
++    OFCHECK_EQUAL(s, "_");
++}
++
+ OFTEST(ofstd_safeSubtractAddMult)
+ {
+   // --------------- Subtraction ----------------


=====================================
debian/patches/0024-CVE-2026-44628a.patch
=====================================
@@ -0,0 +1,114 @@
+Applied-Upstream: f4e0074682645b1a4289d62581926c4394d5c6d5
+Author: Michael Onken <onken at open-connections.de>
+Last-Update: 2026-05-29
+Description: Fix wlmscpfs crash on VR-spoofed seq. attributes.
+ wldsfs.cc cast findAndGetElement() results to DcmSequenceOfItems*
+ without checking the VR. A C-FIND with a dictionary-SQ tag declared
+ under a non-SQ wire VR (Explicit VR) thus dispatched through the wrong
+ vtable and crashes the SCP (see also CVE-2024-28130 in dcmpstat).
+ .
+ Use findAndGetSequence(), which validates the VR. Also guard getItem(0)
+ against an empty ScheduledProcedureStepSequence and drop a bogus cast.
+ .
+ Thanks for the report and analysis to Abhinav Agarwal.
+ .
+ This closes DCMTK issue #1218.
+Reviewed-By: Étienne Mollier <emollier at debian.org>
+Bug-Debian: https://bugs.debian.org/1141411
+
+--- dcmtk.orig/dcmwlm/libsrc/wldsfs.cc
++++ dcmtk/dcmwlm/libsrc/wldsfs.cc
+@@ -1,6 +1,6 @@
+ /*
+  *
+- *  Copyright (C) 1996-2023, OFFIS e.V.
++ *  Copyright (C) 1996-2026, OFFIS e.V.
+  *  All rights reserved.  See COPYRIGHT file for details.
+  *
+  *  This software and supporting documentation were developed by
+@@ -173,20 +173,23 @@
+ //                codeSequenceTagKey - [in] DcmTagKey of the codeSequence attribute which shall be checked.
+ // Return Value : none.
+ {
+-  DcmElement *codeSequenceAttribute = NULL, *descriptionAttribute = NULL;
++  DcmSequenceOfItems *codeSequenceAttribute = NULL;
++  DcmElement *descriptionAttribute = NULL;
+   DcmElement *elementToRemove = NULL, *codeValueAttribute = NULL, *codingSchemeDesignatorAttribute = NULL;
+   OFBool codeSequenceAttributeRemoved = OFFalse;
+ 
+-  // only do something with the code sequence attribute if it is contained in the dataset
+-  if( dataset->findAndGetElement( codeSequenceTagKey, codeSequenceAttribute ).good() )
++  // only do something with the code sequence attribute if it is contained in the dataset.
++  // Note: findAndGetSequence() verifies the element's VR (SQ); a malformed element with a
++  // dictionary-SQ tag but a non-SQ wire VR is rejected here instead of being cast blindly.
++  if( dataset->findAndGetSequence( codeSequenceTagKey, codeSequenceAttribute ).good() )
+   {
+     // if the code sequence attribute is empty or contains exactly one item with an empty
+     // CodeValue and an empty CodingSchemeDesignator, remove the attribute from the dataset
+-    if( ( ((DcmSequenceOfItems*)codeSequenceAttribute)->card() == 0 ) ||
+-        ( ((DcmSequenceOfItems*)codeSequenceAttribute)->card() == 1 &&
+-          ((DcmSequenceOfItems*)codeSequenceAttribute)->getItem(0)->findAndGetElement( DCM_CodeValue, codeValueAttribute ).good() &&
++    if( ( codeSequenceAttribute->card() == 0 ) ||
++        ( codeSequenceAttribute->card() == 1 &&
++          codeSequenceAttribute->getItem(0)->findAndGetElement( DCM_CodeValue, codeValueAttribute ).good() &&
+           codeValueAttribute->getLength() == 0 &&
+-          ((DcmSequenceOfItems*)codeSequenceAttribute)->getItem(0)->findAndGetElement( DCM_CodingSchemeDesignator, codingSchemeDesignatorAttribute ).good() &&
++          codeSequenceAttribute->getItem(0)->findAndGetElement( DCM_CodingSchemeDesignator, codingSchemeDesignatorAttribute ).good() &&
+           codingSchemeDesignatorAttribute->getLength() == 0 ) )
+     {
+       elementToRemove = dataset->remove( codeSequenceAttribute );
+@@ -223,18 +226,21 @@
+ //                sequenceTagKey  - [in] DcmTagKey of the sequence attribute which shall be checked.
+ // Return Value : none.
+ {
+-  DcmElement *sequenceAttribute = NULL, *referencedSOPClassUIDAttribute = NULL, *referencedSOPInstanceUIDAttribute = NULL;
++  DcmSequenceOfItems *sequenceAttribute = NULL;
++  DcmElement *referencedSOPClassUIDAttribute = NULL, *referencedSOPInstanceUIDAttribute = NULL;
+ 
+   // in case the sequence attribute contains exactly one item with an empty
+-  // ReferencedSOPClassUID and an empty ReferencedSOPInstanceUID, remove the item
+-  if( dataset->findAndGetElement( sequenceTagKey, sequenceAttribute ).good() &&
+-      ( (DcmSequenceOfItems*)sequenceAttribute )->card() == 1 &&
+-      ( (DcmSequenceOfItems*)sequenceAttribute )->getItem(0)->findAndGetElement( DCM_ReferencedSOPClassUID, referencedSOPClassUIDAttribute ).good() &&
++  // ReferencedSOPClassUID and an empty ReferencedSOPInstanceUID, remove the item.
++  // Note: findAndGetSequence() verifies the element's VR (SQ); a malformed element with a
++  // dictionary-SQ tag but a non-SQ wire VR is rejected here instead of being cast blindly.
++  if( dataset->findAndGetSequence( sequenceTagKey, sequenceAttribute ).good() &&
++      sequenceAttribute->card() == 1 &&
++      sequenceAttribute->getItem(0)->findAndGetElement( DCM_ReferencedSOPClassUID, referencedSOPClassUIDAttribute ).good() &&
+       referencedSOPClassUIDAttribute->getLength() == 0 &&
+-      ( (DcmSequenceOfItems*)sequenceAttribute )->getItem(0)->findAndGetElement( DCM_ReferencedSOPInstanceUID, referencedSOPInstanceUIDAttribute, OFFalse ).good() &&
++      sequenceAttribute->getItem(0)->findAndGetElement( DCM_ReferencedSOPInstanceUID, referencedSOPInstanceUIDAttribute, OFFalse ).good() &&
+       referencedSOPInstanceUIDAttribute->getLength() == 0 )
+   {
+-    DcmItem *item = ((DcmSequenceOfItems*)sequenceAttribute)->remove( ((DcmSequenceOfItems*)sequenceAttribute)->getItem(0) );
++    DcmItem *item = sequenceAttribute->remove( sequenceAttribute->getItem(0) );
+     delete item;
+   }
+ }
+@@ -259,7 +265,7 @@
+ //                WLM_FAILED_IDENTIFIER_DOES_NOT_MATCH_SOP_CLASS - Error in the search mask encountered.
+ {
+   unsigned long i, j;
+-  DcmElement *scheduledProcedureStepSequenceAttribute = NULL;
++  DcmSequenceOfItems *scheduledProcedureStepSequenceAttribute = NULL;
+ 
+   // Initialize offending elements, error elements and error comment.
+   delete offendingElements;
+@@ -354,9 +360,13 @@
+       }
+ 
+       // if the ScheduledProcedureStepSequence can be found in the current dataset, handle
+-      // existent but empty ScheduledProcedureStepDescription and ScheduledProtocolCodeSequence
+-      if( resultRecord->findAndGetElement( DCM_ScheduledProcedureStepSequence, scheduledProcedureStepSequenceAttribute, OFFalse ).good() )
+-        HandleExistentButEmptyDescriptionAndCodeSequenceAttributes( ((DcmDataset*)((DcmSequenceOfItems*)scheduledProcedureStepSequenceAttribute)->getItem(0)), DCM_ScheduledProcedureStepDescription, DCM_ScheduledProtocolCodeSequence );
++      // existent but empty ScheduledProcedureStepDescription and ScheduledProtocolCodeSequence.
++      // Note: findAndGetSequence() verifies the element's VR (SQ); a malformed element with a
++      // dictionary-SQ tag but a non-SQ wire VR is rejected here instead of being cast blindly.
++      // Also guard against an empty sequence so that getItem(0) does not return NULL.
++      if( resultRecord->findAndGetSequence( DCM_ScheduledProcedureStepSequence, scheduledProcedureStepSequenceAttribute, OFFalse ).good() &&
++          scheduledProcedureStepSequenceAttribute->card() > 0 )
++        HandleExistentButEmptyDescriptionAndCodeSequenceAttributes( scheduledProcedureStepSequenceAttribute->getItem(0), DCM_ScheduledProcedureStepDescription, DCM_ScheduledProtocolCodeSequence );
+ 
+       // handle existent but empty RequestedProcedureDescription and RequestedProcedureCodeSequence
+       HandleExistentButEmptyDescriptionAndCodeSequenceAttributes( resultRecord, DCM_RequestedProcedureDescription, DCM_RequestedProcedureCodeSequence );


=====================================
debian/patches/0025-CVE-2026-44628b.patch
=====================================
@@ -0,0 +1,80 @@
+Applied-Upstream: 694a0a06a38015ce768fa161a62b148189f84959
+Author: Michael Onken <onken at open-connections.de>
+Last-Update: 2026-05-29
+Description: Fix VR-spoofed sequence cast crash in wlfsim, too.
+ Apply the same findAndGetSequence() fix to the unchecked casts in
+ DatasetIsComplete() and ReferencedStudyOrPatientSequenceIsAbsent...()
+ as in f4e0074.
+Reviewed-By: Étienne Mollier <emollier at debian.org>
+Bug-Debian: https://bugs.debian.org/1141411
+
+diff --git a/dcmwlm/libsrc/wlfsim.cc b/dcmwlm/libsrc/wlfsim.cc
+index 40a93c4eb..8543f795a 100644
+--- a/dcmwlm/libsrc/wlfsim.cc
++++ b/dcmwlm/libsrc/wlfsim.cc
+@@ -493,7 +493,7 @@ OFBool WlmFileSystemInteractionManager::DatasetIsComplete( DcmDataset *dataset )
+ // Return Value : OFTrue in case the given dataset contains all necessary return type 1 information,
+ //                OFFalse otherwise.
+ {
+-  DcmElement *scheduledProcedureStepSequence = NULL;
++  DcmSequenceOfItems *scheduledProcedureStepSequence = NULL;
+ 
+   // initialize returnValue
+   OFBool complete = OFTrue;
+@@ -503,7 +503,7 @@ OFBool WlmFileSystemInteractionManager::DatasetIsComplete( DcmDataset *dataset )
+   // the dataset is considered to be incomplete...
+   // ...if the ScheduledProcedureStepSequence is missing or
+   // ...if the ScheduledProcedureStepSequence does not have exactly one item
+-  if( dataset->findAndGetElement( DCM_ScheduledProcedureStepSequence, scheduledProcedureStepSequence ).bad() || ((DcmSequenceOfItems*)scheduledProcedureStepSequence)->card() != 1 )
++  if( dataset->findAndGetSequence( DCM_ScheduledProcedureStepSequence, scheduledProcedureStepSequence ).bad() || scheduledProcedureStepSequence->card() != 1 )
+   {
+     DCMWLM_DEBUG("- ScheduledProcedureStepSequence " << DCM_ScheduledProcedureStepSequence << " is missing or does not have exactly one item");
+     complete = OFFalse;
+@@ -512,7 +512,7 @@ OFBool WlmFileSystemInteractionManager::DatasetIsComplete( DcmDataset *dataset )
+   {
+     // so the ScheduledProcedureStepSequence is existent and has exactly one item;
+     // get this one and only item from the ScheduledProcedureStepSequence
+-    DcmItem *scheduledProcedureStepSequenceItem = ((DcmSequenceOfItems*)scheduledProcedureStepSequence)->getItem(0);
++    DcmItem *scheduledProcedureStepSequenceItem = scheduledProcedureStepSequence->getItem(0);
+ 
+     // the dataset is considered to be incomplete...
+     // ...if ScheduledStationAETitle is missing or empty in the ScheduledProcedureStepSequence, or
+@@ -563,11 +563,14 @@ OFBool WlmFileSystemInteractionManager::ReferencedStudyOrPatientSequenceIsAbsent
+ // Return Value : OFTrue in case the sequence attribute is absent (and cannot be added to the dataset)
+ //                or existent but non-empty and incomplete, OFFalse otherwise.
+ {
+-  DcmElement *sequence = NULL;
++  DcmSequenceOfItems *sequence = NULL;
+   OFBool result;
+ 
+   // check whether the type 2 sequence attribute is absent
+-  if( dset->findAndGetElement( sequenceTagKey, sequence ).bad() )
++  // (findAndGetSequence also fails if the attribute is present but does not have
++  //  a sequence VR, e.g. due to a VR-spoofed Explicit VR encoding; in that case
++  //  the malformed element is replaced below with a proper empty sequence)
++  if( dset->findAndGetSequence( sequenceTagKey, sequence ).bad() )
+   {
+     DCMWLM_DEBUG("- " << DcmTag(sequenceTagKey).getTagName() << " " << sequenceTagKey << " is missing");
+     // try to add it to the dataset and return OFFalse if successful
+@@ -583,17 +586,17 @@ OFBool WlmFileSystemInteractionManager::ReferencedStudyOrPatientSequenceIsAbsent
+   {
+     // if the sequence attribute is existent but empty, we want to return OFFalse
+     // (note that the sequence is actually type 2, so being empty is ok)
+-    if( ((DcmSequenceOfItems*)sequence)->card() == 0 )
++    if( sequence->card() == 0 )
+       result = OFFalse;
+     else
+     {
+       // if the sequence attribute is existent and non-empty, we need
+       // to check every item in the sequence for completeness
+       result = OFFalse;
+-      for( unsigned long i=0 ; i<((DcmSequenceOfItems*)sequence)->card() && !result ; i++ )
++      for( unsigned long i=0 ; i<sequence->card() && !result ; i++ )
+       {
+-        if( AttributeIsAbsentOrEmpty( DCM_ReferencedSOPClassUID, ((DcmSequenceOfItems*)sequence)->getItem(i) ) ||
+-            AttributeIsAbsentOrEmpty( DCM_ReferencedSOPInstanceUID, ((DcmSequenceOfItems*)sequence)->getItem(i) ) )
++        if( AttributeIsAbsentOrEmpty( DCM_ReferencedSOPClassUID, sequence->getItem(i) ) ||
++            AttributeIsAbsentOrEmpty( DCM_ReferencedSOPInstanceUID, sequence->getItem(i) ) )
+           result = OFTrue;
+       }
+       if ( result )


=====================================
debian/patches/series
=====================================
@@ -15,3 +15,9 @@ remove_version.patch
 0017-CVE-2025-14841.patch
 0018-CVE-2026-10194.patch
 0019-CVE-2026-12805.patch
+0020-CVE-2026-50003.patch
+0021-CVE-2026-50254.patch
+0022-CVE-2026-35505.patch
+0023-CVE-2026-52868.patch
+0024-CVE-2026-44628a.patch
+0025-CVE-2026-44628b.patch



View it on GitLab: https://salsa.debian.org/med-team/dcmtk/-/compare/a83229fb604786c30145c04a02ad1c2e19952d0e...988ba8fa609150214f73c69cf9beaef6e9662c1e

-- 
View it on GitLab: https://salsa.debian.org/med-team/dcmtk/-/compare/a83229fb604786c30145c04a02ad1c2e19952d0e...988ba8fa609150214f73c69cf9beaef6e9662c1e
You're receiving this email because of your account on salsa.debian.org. Manage all notifications: https://salsa.debian.org/-/profile/notifications | Help: https://salsa.debian.org/help


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/debian-med-commit/attachments/20260707/94b13c12/attachment-0001.htm>


More information about the debian-med-commit mailing list