# HG changeset patch # User Sebastien Jodogne # Date 1500392006 -7200 # Node ID 3ab96768d1445679d17e3fe0174ff35ebe761983 # Parent 56504f89d4ac672ebcde78e311faf3632d85c443 Fix issue 52 (DICOM level security association problems) diff -r 56504f89d4ac -r 3ab96768d144 NEWS --- a/NEWS Tue Jul 18 08:41:09 2017 +0200 +++ b/NEWS Tue Jul 18 17:33:26 2017 +0200 @@ -7,12 +7,12 @@ * Orthanc now anonymizes according to Basic Profile of PS 3.15-2017c Table E.1-1 * In the "DicomModalities" configuration: - Manufacturer type MedInria is now obsolete - - Manufacturer types AgfaImpax and SyngoVia are obsolete too, - use GenericNoWildcardInDates instead + - Manufacturer types AgfaImpax and SyngoVia are obsolete too + (use GenericNoWildcardInDates instead) - Obsolete manufacturers are still accepted but might disappear in the future - - Added new manufacturer: GenericNoWidlcards to replace all '*' by '' in + - Added new manufacturer: GenericNoUniversalWildcard to replace all '*' by '' in outgoing C-Find requests -* "Locale" configuration option +* New security-related options: "DicomAlwaysAllowStore" and "DicomCheckModalityHost" REST API -------- @@ -46,6 +46,7 @@ * Fix issue 45 (crash when providing a folder to "--config" command-line option) * Fix issue 46 (PHI remaining after anonymization) * Fix issue 49 (worklists: accentuated characters are removed from C-Find responses) +* Fix issue 52 (DICOM level security association problems) * Fix issue 55 (modification/anonymization of tags that might break the database model now requires the "Force" parameter to be set to "true" in the query) * Fix issue 56 (case-insensitive matching over accents) @@ -53,7 +54,8 @@ * Fix XSS inside DICOM in Orthanc Explorer (as reported by Victor Pasnkel, Morphus Labs) * Upgrade to DCMTK 3.6.2 in static builds (released on 2017-07-17) * Upgrade to Boost 1.64.0 in static builds -* Removed configuration option USE_DCMTK_361_PRIVATE_DIC +* New advanced "Locale" configuration option +* Removed configuration option "USE_DCMTK_361_PRIVATE_DIC" Version 1.2.0 (2016/12/13) diff -r 56504f89d4ac -r 3ab96768d144 OrthancServer/DicomProtocol/DicomUserConnection.cpp --- a/OrthancServer/DicomProtocol/DicomUserConnection.cpp Tue Jul 18 08:41:09 2017 +0200 +++ b/OrthancServer/DicomProtocol/DicomUserConnection.cpp Tue Jul 18 17:33:26 2017 +0200 @@ -496,7 +496,7 @@ switch (manufacturer) { case ModalityManufacturer_GenericNoWildcardInDates: - case ModalityManufacturer_GenericNoWildcards: + case ModalityManufacturer_GenericNoUniversalWildcard: { std::auto_ptr fix(fields.Clone()); @@ -508,7 +508,7 @@ // Replace a "*" wildcard query by an empty query ("") for // "date" or "all" value representations depending on the // type of manufacturer. - if (manufacturer == ModalityManufacturer_GenericNoWildcards || + if (manufacturer == ModalityManufacturer_GenericNoUniversalWildcard || (manufacturer == ModalityManufacturer_GenericNoWildcardInDates && FromDcmtkBridge::LookupValueRepresentation(*it) == ValueRepresentation_Date)) { diff -r 56504f89d4ac -r 3ab96768d144 OrthancServer/Internals/CommandDispatcher.cpp --- a/OrthancServer/Internals/CommandDispatcher.cpp Tue Jul 18 08:41:09 2017 +0200 +++ b/OrthancServer/Internals/CommandDispatcher.cpp Tue Jul 18 17:33:26 2017 +0200 @@ -806,13 +806,12 @@ // Check whether this request is allowed by the security filter if (supported && - request != DicomRequestType_Echo && // Always allow incoming ECHO requests filter_ != NULL && !filter_->IsAllowedRequest(remoteIp_, remoteAet_, calledAet_, request)) { - LOG(ERROR) << EnumerationToString(request) - << " requests are disallowed for the AET \"" - << remoteAet_ << "\""; + LOG(WARNING) << "Rejected " << EnumerationToString(request) + << " request from remote DICOM modality with AET \"" + << remoteAet_ << "\" and hostname \"" << remoteIp_ << "\""; cond = DIMSE_ILLEGALASSOCIATION; supported = false; finished = true; @@ -906,7 +905,7 @@ else { OFString temp_str; - LOG(ERROR) << "DIMSE failure (aborting association): " << cond.text(); + LOG(INFO) << "DIMSE failure (aborting association): " << cond.text(); /* some kind of error so abort the association */ ASC_abortAssociation(assoc_); } diff -r 56504f89d4ac -r 3ab96768d144 OrthancServer/OrthancInitialization.cpp --- a/OrthancServer/OrthancInitialization.cpp Tue Jul 18 08:41:09 2017 +0200 +++ b/OrthancServer/OrthancInitialization.cpp Tue Jul 18 17:33:26 2017 +0200 @@ -855,10 +855,30 @@ } - bool Configuration::IsKnownAETitle(const std::string& aet) + bool Configuration::IsKnownAETitle(const std::string& aet, + const std::string& ip) { RemoteModalityParameters modality; - return LookupDicomModalityUsingAETitle(modality, aet); + + if (!LookupDicomModalityUsingAETitle(modality, aet)) + { + LOG(WARNING) << "Modality \"" << aet + << "\" is not listed in the \"DicomModalities\" configuration option"; + return false; + } + else if (!Configuration::GetGlobalBoolParameter("DicomCheckModalityHost", false) || + ip == modality.GetHost()) + { + return true; + } + else + { + LOG(WARNING) << "Forbidding access from AET \"" << aet + << "\" given its hostname (" << ip << ") does not match " + << "the \"DicomModalities\" configuration option (" + << modality.GetHost() << " was expected)"; + return false; + } } diff -r 56504f89d4ac -r 3ab96768d144 OrthancServer/OrthancInitialization.h --- a/OrthancServer/OrthancInitialization.h Tue Jul 18 08:41:09 2017 +0200 +++ b/OrthancServer/OrthancInitialization.h Tue Jul 18 17:33:26 2017 +0200 @@ -106,7 +106,8 @@ static void GetGlobalListOfStringsParameter(std::list& target, const std::string& key); - static bool IsKnownAETitle(const std::string& aet); + static bool IsKnownAETitle(const std::string& aet, + const std::string& ip); static bool IsSameAETitle(const std::string& aet1, const std::string& aet2); diff -r 56504f89d4ac -r 3ab96768d144 OrthancServer/ServerEnumerations.cpp --- a/OrthancServer/ServerEnumerations.cpp Tue Jul 18 08:41:09 2017 +0200 +++ b/OrthancServer/ServerEnumerations.cpp Tue Jul 18 17:33:26 2017 +0200 @@ -328,8 +328,8 @@ case ModalityManufacturer_GenericNoWildcardInDates: return "GenericNoWildcardInDates"; - case ModalityManufacturer_GenericNoWildcards: - return "GenericNoWildcards"; + case ModalityManufacturer_GenericNoUniversalWildcard: + return "GenericNoUniversalWildcard"; case ModalityManufacturer_StoreScp: return "StoreScp"; @@ -393,9 +393,9 @@ { return ModalityManufacturer_GenericNoWildcardInDates; } - else if (manufacturer == "GenericNoWildcards") + else if (manufacturer == "GenericNoUniversalWildcard") { - return ModalityManufacturer_GenericNoWildcards; + return ModalityManufacturer_GenericNoUniversalWildcard; } else if (manufacturer == "ClearCanvas") { diff -r 56504f89d4ac -r 3ab96768d144 OrthancServer/ServerEnumerations.h --- a/OrthancServer/ServerEnumerations.h Tue Jul 18 08:41:09 2017 +0200 +++ b/OrthancServer/ServerEnumerations.h Tue Jul 18 17:33:26 2017 +0200 @@ -60,7 +60,7 @@ { ModalityManufacturer_Generic, ModalityManufacturer_GenericNoWildcardInDates, - ModalityManufacturer_GenericNoWildcards, + ModalityManufacturer_GenericNoUniversalWildcard, ModalityManufacturer_StoreScp, ModalityManufacturer_ClearCanvas, ModalityManufacturer_Dcm4Chee, diff -r 56504f89d4ac -r 3ab96768d144 OrthancServer/main.cpp --- a/OrthancServer/main.cpp Tue Jul 18 08:41:09 2017 +0200 +++ b/OrthancServer/main.cpp Tue Jul 18 17:33:26 2017 +0200 @@ -158,27 +158,33 @@ { } - virtual bool IsAllowedConnection(const std::string& /*remoteIp*/, - const std::string& /*remoteAet*/, - const std::string& /*calledAet*/) + virtual bool IsAllowedConnection(const std::string& remoteIp, + const std::string& remoteAet, + const std::string& calledAet) { + LOG(INFO) << "Incoming connection from AET " << remoteAet + << " on IP " << remoteIp << ", calling AET " << calledAet; + return true; } - virtual bool IsAllowedRequest(const std::string& /*remoteIp*/, + virtual bool IsAllowedRequest(const std::string& remoteIp, const std::string& remoteAet, - const std::string& /*calledAet*/, + const std::string& calledAet, DicomRequestType type) { - if (type == DicomRequestType_Store) + LOG(INFO) << "Incoming " << Orthanc::EnumerationToString(type) << " request from AET " + << remoteAet << " on IP " << remoteIp << ", calling AET " << calledAet; + + if (type == DicomRequestType_Store && + Configuration::GetGlobalBoolParameter("DicomAlwaysAllowStore", true)) { // Incoming store requests are always accepted, even from unknown AET return true; } - if (!Configuration::IsKnownAETitle(remoteAet)) + if (!Configuration::IsKnownAETitle(remoteAet, remoteIp)) { - LOG(ERROR) << "Unknown remote DICOM modality AET: \"" << remoteAet << "\""; return false; } else diff -r 56504f89d4ac -r 3ab96768d144 Resources/Configuration.json --- a/Resources/Configuration.json Tue Jul 18 08:41:09 2017 +0200 +++ b/Resources/Configuration.json Tue Jul 18 17:33:26 2017 +0200 @@ -82,7 +82,8 @@ // The DICOM Application Entity Title "DicomAet" : "ORTHANC", - // Check whether the called AET corresponds during a DICOM request + // Check whether the called AET corresponds to the AET of Orthanc + // during an incoming DICOM SCU request "DicomCheckCalledAet" : false, // The DICOM port @@ -156,18 +157,31 @@ // "sample" : [ "STORESCP", "127.0.0.1", 2000 ] /** - * A fourth parameter is available to enable patches for a - * specific PACS manufacturer. The allowed values are currently - * "Generic" (default value), - * "GenericNoWildcardInDates" (to replace '*' by '' in date fields in outgoing C-Find) - * "GenericNoWildcards" (to replace '*' by '' in all fields in outgoing C-Find) - * "StoreScp" (storescp tool from DCMTK), - * "ClearCanvas", "Dcm4Chee" and "Vitrea". + * A fourth parameter is available to enable patches for + * specific PACS manufacturers. The allowed values are currently: + * - "Generic" (default value), + * - "GenericNoWildcardInDates" (to replace "*" by "" in date fields + * in outgoing C-Find requests originating from Orthanc) + * - "GenericNoUniversalWildcard" (to replace "*" by "" in all fields + * in outgoing C-Find SCU requests originating from Orthanc) + * - "StoreScp" (storescp tool from DCMTK), + * - "ClearCanvas", "Dcm4Chee" and "Vitrea". * This parameter is case-sensitive. **/ // "clearcanvas" : [ "CLEARCANVAS", "192.168.1.1", 104, "ClearCanvas" ] }, + // Whether the Orthanc SCP allows incoming C-Store requests, even + // from SCU modalities it does not know about (i.e. that are not + // listed in the "DicomModalities" option above) + "DicomAlwaysAllowStore" : true, + + // Whether Orthanc checks the IP/hostname address of the remote + // modality initiating a DICOM connection (as listed in the + // "DicomModalities" option above). If this option is set to + // "false", Orthanc only checks the AET of the remote modality. + "DicomCheckModalityHost" : false, + // The timeout (in seconds) after which the DICOM associations are // considered as closed by the Orthanc SCU (client) if the remote // DICOM SCP (server) does not answer. diff -r 56504f89d4ac -r 3ab96768d144 UnitTestsSources/UnitTestsMain.cpp --- a/UnitTestsSources/UnitTestsMain.cpp Tue Jul 18 08:41:09 2017 +0200 +++ b/UnitTestsSources/UnitTestsMain.cpp Tue Jul 18 17:33:26 2017 +0200 @@ -528,7 +528,7 @@ ASSERT_STREQ("Generic", EnumerationToString(StringToModalityManufacturer("Generic"))); ASSERT_STREQ("GenericNoWildcardInDates", EnumerationToString(StringToModalityManufacturer("GenericNoWildcardInDates"))); - ASSERT_STREQ("GenericNoWildcards", EnumerationToString(StringToModalityManufacturer("GenericNoWildcards"))); + ASSERT_STREQ("GenericNoUniversalWildcard", EnumerationToString(StringToModalityManufacturer("GenericNoUniversalWildcard"))); ASSERT_STREQ("StoreScp", EnumerationToString(StringToModalityManufacturer("StoreScp"))); ASSERT_STREQ("ClearCanvas", EnumerationToString(StringToModalityManufacturer("ClearCanvas"))); ASSERT_STREQ("Dcm4Chee", EnumerationToString(StringToModalityManufacturer("Dcm4Chee")));