changeset 790:9904dba50f88 default tip

new AllowedHosts configuration
author Alain Mazy <am@orthanc.team>
date Mon, 21 Sep 2026 18:35:01 +0200
parents 67080662c302
children
files NEWS Plugin/Configuration.cpp
diffstat 2 files changed, 47 insertions(+), 1 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Thu Sep 17 14:12:41 2026 +0200
+++ b/NEWS	Mon Sep 21 18:35:01 2026 +0200
@@ -1,3 +1,12 @@
+Pending changes in the mainline
+===============================
+
+* New configuration "AllowedHosts" that defines a list of trusted hosts for the
+  "Host", "X-Forwarded-Host", "Forwarded" HTTP headers.  
+  POSSIBLE BREAKING CHANGE: if Orthanc is behind a reverse proxy, make sure to either
+  define "Host" or "AllowedHosts" in the "DicomWeb" configuration section.
+
+
 Version 1.24 (2026-08-19)
 =========================
 
--- a/Plugin/Configuration.cpp	Thu Sep 17 14:12:41 2026 +0200
+++ b/Plugin/Configuration.cpp	Mon Sep 21 18:35:01 2026 +0200
@@ -35,6 +35,7 @@
 #include <boost/regex.hpp>
 #include <boost/lexical_cast.hpp>
 #include <boost/algorithm/string/predicate.hpp>
+#include <boost/regex.hpp>
 
 
 // Assume Latin-1 encoding by default (as in the Orthanc core)
@@ -533,14 +534,34 @@
     }
 
 
+    static bool IsAllowedHost(const std::string& host, const std::set<std::string>& allowedHosts)
+    {
+      bool foundTrustedHost = false;
+      for (std::set<std::string>::const_iterator
+            allowedHost = allowedHosts.begin(); allowedHost != allowedHosts.end(); ++allowedHost)
+      {
+        boost::regex pattern(Orthanc::Toolbox::WildcardToRegularExpression(*allowedHost));
+
+        if (boost::regex_match(host, pattern))
+        {
+          foundTrustedHost = true;
+          break;
+        }
+      }
+
+      return foundTrustedHost;
+    }
+
     std::string GetBasePublicUrl(const HttpHeaders& headers)
     {
       assert(dicomWebConfiguration_.get() != NULL);
       std::string host = dicomWebConfiguration_->GetStringValue("Host", "");
+      std::set<std::string> allowedHosts;
+      dicomWebConfiguration_->LookupSetOfStrings(allowedHosts, "AllowedHosts", "");
       bool https = dicomWebConfiguration_->GetBooleanValue("Ssl", false);
 
       std::string forwardedHost, forwardedProto;
-      if (host.empty() &&
+      if (host.empty() && 
           LookupHttpHeader2(forwardedHost, headers, "x-forwarded-host") &&
           LookupHttpHeader2(forwardedProto, headers, "x-forwarded-proto"))
       {
@@ -599,6 +620,22 @@
         host = "localhost:8042";
       }
 
+      if (allowedHosts.size() == 0)
+      {
+        if (!boost::starts_with(host, "localhost") && !boost::starts_with(host, "127.0.0.1"))  // always trust localhost
+        {
+          throw Orthanc::OrthancException(
+            Orthanc::ErrorCode_InternalError,
+            std::string("DICOMWeb plugin: no 'Host' defined and no 'AllowedHosts' defined although there are forwarded HTTP headers.  Unable to trust the forwarded HTTP headers for host '") + host + "'.");
+        }
+      }
+      else if (!IsAllowedHost(host, allowedHosts))
+      {
+        throw Orthanc::OrthancException(
+          Orthanc::ErrorCode_InternalError,
+          std::string("DICOMWeb plugin: no 'Host' defined and the forwarded HTTP headers did not match any of the 'AllowedHosts'.  Unable to trust the forwarded HTTP headers for host '") + host + "'.");
+      }
+
       return (https ? "https://" : "http://") + host + GetPublicRoot();
     }