Package: release.debian.org
Severity: normal
Tags: trixie
X-Debbugs-Cc: [email protected]
Control: affects -1 + src:dcmtk
User: [email protected]
Usertags: pu

Hi Stable Release Managers,

[ Reason ]
dcmtk 3.6.9-5+deb13u2 in trixie is affected by the following
vulnerabilities, reported via #1141411 by the security team:

  * CVE-2026-50003: a risk of path traversal,
  * CVE-2026-50254: a risk of denial of service,
  * CVE-2026-35505: a risk of denial of service,
  * CVE-2026-52868: risks of information leak,
  * CVE-2026-44628: a risk of denial of service.

Our assessment is that a proposed update is preferable in that
case, instead of going through a Debian Security Advisory, hence
the present request.

[ Impact ]
If the update is not approved, dcmtk will remain affected by the
aforementioned issues.

[ Tests ]
The mitigations agaist CVE-2026-35505 and CVE-2026-52868 are
accompanied by updates of the test suite, which is run at build
time.  Not all mitigations shipped with test items though.

I have also made sure that the changes were not affecting
reverse dependencies, by running their autopkgtest, for those
which have one.  While I was at it, I also verified direct
reverse build dependencies were rebuilding fine.

[ Risks ]
There are a lot of changes and I don't necessarily make properly
sense of everything that has been modified.

On the other hand, the issues are fixed in unstable and the
package shown no difficulties migrating to forky.  For now there
were no bug reports about regressions caused by the changes.

[ Checklist ]
  [*] *all* changes are documented in the d/changelog
  [*] I reviewed all changes and I approve them
  [*] attach debdiff against the package in stable
  [*] the issue is verified as fixed in unstable

[ Changes ]
0020-CVE-2026-50003.patch contains upstream commit fixing the
risk of path traversal.  It does so by defining uidForFilename,
sanitizing it and use it for the rest of the processing.

0021-CVE-2026-50254.patch contains upstream commit fixing the
leak causing the risk of denial of service.  It does so by
adding the missing code to free appropriately the extNeg object
and return (presumably in error) before the function/method goes
further.  The upstream patch is cluttered by adjustments to
comments.  I left them to facilitate comparison, should anyone
want to audit my changes based on upstream commits.

0022-CVE-2026-35505.patch contains upstream commit fixing the
leak causing the second risk of denial of service.  It does so
by invoking the relevant destructor functions before returning
the errors in the branchs missing the ressource freeing steps.
This patch also introduces test items, resulting in testable
code, but also inflated patch.

0023-CVE-2026-52868.patch contains upstream commit fixing the 
risks of information leak.  It does so by dropping a buggy
sanitizeAETitle function local to the scope of the file
dcmnet/apps/storescp.cc and replaces it by a new generic
counterpart OFStandard::sanitizeAETitle where invoked.  Given
that tackling the second leak slightly changes the behavior of
the wlmcpfs, the manual page is also adjusted.  Remark that this
change introduces a new symbol IsValidAETitleForFilesystem via
the header file dcmwlm/include/dcmtk/dcmwlm/wlfsim.h, therefore
breaking reverse compatibility with the older abi I guess.  The
next hunk implements the method IsValidAETitleForFilesystem,
which checks for various basics about file name validity and
sprinkles it at various locations where changes are needed.
Addition of relevant test items in the upstream test suite
contributes to a fair fraction of the rest of the patch size.
Note there is a change to ofstd/include/dcmtk/ofstd/ofstd.h
hidden after test hunks, which declares the sanitizeAETitle
redefined earlier in the patch.  dcmtk/ofstd/libsrc/ofstd.cc
shows where the fixed OFStandard::sanitizeAETitle has been
implemented.  Further tests for the new sanitizer are defined,
and that's the end of the patch.

0024-CVE-2026-44628a.patch contains upstream commit fixing part
of the third risk of denial of service.  I have difficulties
making sense of what happens here.  If I understand correctly,
a number of findAndGetElement invocations get replaced by
findAndGetSequence, causing further changes in the hunks to
eventually adjust to the new type; interestingly, there are
cases where this resolves uses of existing type casts,
suggesting the use of findAndGetSequence would have been more
appropriate in the first place.

0025-CVE-2026-44628b.patch contains upstream commit fixing the
second part of the third risk of denial of service.  It extend
the previously introduced fix at a number of further locations.

[ Other info ]
Have a nice day,  :)
-- 
  .''`.  Étienne Mollier <[email protected]>
 : :' :  pgp: 8f91 b227 c7d6 f2b1 948c  8236 793c f67e 8f0d 11da
 `. `'   sent from /dev/pts/1, please excuse my verbosity
   `-
diff -Nru dcmtk-3.6.9/debian/changelog dcmtk-3.6.9/debian/changelog
--- dcmtk-3.6.9/debian/changelog        2026-06-23 21:44:21.000000000 +0200
+++ dcmtk-3.6.9/debian/changelog        2026-07-07 22:38:06.000000000 +0200
@@ -1,3 +1,16 @@
+dcmtk (3.6.9-5+deb13u3) trixie; urgency=medium
+
+  * Team upload.
+  * 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 <[email protected]>  Tue, 07 Jul 2026 22:38:06 +0200
+
 dcmtk (3.6.9-5+deb13u2) trixie; urgency=medium
 
   * Team upload.
diff -Nru dcmtk-3.6.9/debian/patches/0020-CVE-2026-50003.patch 
dcmtk-3.6.9/debian/patches/0020-CVE-2026-50003.patch
--- dcmtk-3.6.9/debian/patches/0020-CVE-2026-50003.patch        1970-01-01 
01:00:00.000000000 +0100
+++ dcmtk-3.6.9/debian/patches/0020-CVE-2026-50003.patch        2026-07-07 
20:00:05.000000000 +0200
@@ -0,0 +1,42 @@
+Applied-Upstream: eca9a03dda7d4fc1faa7e5a6dac9617938cf5803
+Author: Michael Onken <[email protected]>
+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 <[email protected]>
+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())
+                 {
diff -Nru dcmtk-3.6.9/debian/patches/0021-CVE-2026-50254.patch 
dcmtk-3.6.9/debian/patches/0021-CVE-2026-50254.patch
--- dcmtk-3.6.9/debian/patches/0021-CVE-2026-50254.patch        1970-01-01 
01:00:00.000000000 +0100
+++ dcmtk-3.6.9/debian/patches/0021-CVE-2026-50254.patch        2026-07-07 
20:00:57.000000000 +0200
@@ -0,0 +1,164 @@
+Applied-Upstream: 23f181f7a3cb8334056f751a3a0c2ddf01046752
+Author: Michael Onken <[email protected]>
+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 <[email protected]>
+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;
+ 
diff -Nru dcmtk-3.6.9/debian/patches/0022-CVE-2026-35505.patch 
dcmtk-3.6.9/debian/patches/0022-CVE-2026-35505.patch
--- dcmtk-3.6.9/debian/patches/0022-CVE-2026-35505.patch        1970-01-01 
01:00:00.000000000 +0100
+++ dcmtk-3.6.9/debian/patches/0022-CVE-2026-35505.patch        2026-07-07 
20:01:09.000000000 +0200
@@ -0,0 +1,509 @@
+Applied-Upstream: 2312891a8d058c862e00bcbd636e5da26308658a
+Author: Michael Onken <[email protected]>
+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 <[email protected]>
+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")
diff -Nru dcmtk-3.6.9/debian/patches/0023-CVE-2026-52868.patch 
dcmtk-3.6.9/debian/patches/0023-CVE-2026-52868.patch
--- dcmtk-3.6.9/debian/patches/0023-CVE-2026-52868.patch        1970-01-01 
01:00:00.000000000 +0100
+++ dcmtk-3.6.9/debian/patches/0023-CVE-2026-52868.patch        2026-07-07 
20:03:29.000000000 +0200
@@ -0,0 +1,746 @@
+commit e3878daf870cd2db50eadfde38615f0afae8a584
+Author: Michael Onken <[email protected]>
+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@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@host-1";
++    OFStandard::sanitizeAETitle(s);
++    OFCHECK_EQUAL(s, "AE.TITLE:1@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 ----------------
diff -Nru dcmtk-3.6.9/debian/patches/0024-CVE-2026-44628a.patch 
dcmtk-3.6.9/debian/patches/0024-CVE-2026-44628a.patch
--- dcmtk-3.6.9/debian/patches/0024-CVE-2026-44628a.patch       1970-01-01 
01:00:00.000000000 +0100
+++ dcmtk-3.6.9/debian/patches/0024-CVE-2026-44628a.patch       2026-07-07 
20:04:16.000000000 +0200
@@ -0,0 +1,114 @@
+Applied-Upstream: f4e0074682645b1a4289d62581926c4394d5c6d5
+Author: Michael Onken <[email protected]>
+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 <[email protected]>
+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 );
diff -Nru dcmtk-3.6.9/debian/patches/0025-CVE-2026-44628b.patch 
dcmtk-3.6.9/debian/patches/0025-CVE-2026-44628b.patch
--- dcmtk-3.6.9/debian/patches/0025-CVE-2026-44628b.patch       1970-01-01 
01:00:00.000000000 +0100
+++ dcmtk-3.6.9/debian/patches/0025-CVE-2026-44628b.patch       2026-07-07 
20:04:27.000000000 +0200
@@ -0,0 +1,80 @@
+Applied-Upstream: 694a0a06a38015ce768fa161a62b148189f84959
+Author: Michael Onken <[email protected]>
+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 <[email protected]>
+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 )
diff -Nru dcmtk-3.6.9/debian/patches/series dcmtk-3.6.9/debian/patches/series
--- dcmtk-3.6.9/debian/patches/series   2026-06-23 21:44:21.000000000 +0200
+++ dcmtk-3.6.9/debian/patches/series   2026-07-07 20:05:27.000000000 +0200
@@ -15,3 +15,9 @@
 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

Attachment: signature.asc
Description: PGP signature

Reply via email to