From 1779287a2d9740d30660c94209e08f1b975dfda4 Mon Sep 17 00:00:00 2001
From: Roel Standaert <roel@emweb.be>
Date: Tue, 22 Sep 2020 15:05:40 +0200
Subject: [PATCH] Issue #7586: configurable trusted proxies and forwarded
 header

Instead of disabling Client-IP headers by default, let's just make
the header to use configurable, and have it default to X-Forwarded-For.

Also, instead of filtering out local IP addresses from X-Forwarded-For
and trying to get the client IP address that way, we'll just go back
to front until we occur an IP address that is not a trusted proxy.

Added options to wt_config.xml:

 - forwarded-header
 - trusted-proxies, comma-separated and in CIDR notation

We will now trust headers coming from a reverse proxy
(e.g. X-Forwarded-* headers, SSL info headers) only if they are coming
from a trusted reverse proxy. This removes the caveat that Wt applications
should not be directly accessed if configured to be behind a reverse proxy.
---
 examples/widgetgallery/approot/wt_config.xml |   5 +
 src/Wt/Http/Request.C                        |   6 +-
 src/Wt/WEnvironment.C                        |  35 ++--
 src/fcgi/FCGIStream.C                        |   2 +-
 src/http/HTTPRequest.C                       |   4 +-
 src/http/HTTPRequest.h                       |   2 +-
 src/http/ProxyReply.C                        |  29 +++-
 src/http/WServer.C                           |   5 +
 src/isapi/IsapiRequest.C                     |   2 +-
 src/isapi/IsapiRequest.h                     |   2 +-
 src/web/Configuration.C                      | 126 +++++++++++++++
 src/web/Configuration.h                      |  25 +++
 src/web/WebRequest.C                         |  64 ++------
 src/web/WebRequest.h                         |   5 +-
 src/web/WebSession.C                         |   3 +-
 src/web/WebSocketMessage.C                   |   4 +-
 src/web/WebSocketMessage.h                   |   2 +-
 test/http/HttpClientServerTest.C             | 159 ++++++++++++++++++-
 test/private/EventDecodeTest.C               |   2 +-
 wt_config.xml.in                             |  17 ++
 20 files changed, 400 insertions(+), 99 deletions(-)

diff --git a/examples/widgetgallery/approot/wt_config.xml b/examples/widgetgallery/approot/wt_config.xml
index 33aed6667..1abae0a55 100644
--- a/examples/widgetgallery/approot/wt_config.xml
+++ b/examples/widgetgallery/approot/wt_config.xml
@@ -19,6 +19,11 @@
         </user-agents>
 
         <web-sockets>true</web-sockets>
+
+        <behind-reverse-proxy>true</behind-reverse-proxy>
+        <forwarded-header>X-Forwarded-For</forwarded-header>
+        <trusted-proxies>127.0.0.1,::1</trusted-proxies>
+
 	<bootstrap-method>
 	  <for path="*">progressive</for>
 	  <for path="/forms/line_text-editor">default</for>
diff --git a/src/Wt/Http/Request.C b/src/Wt/Http/Request.C
index 17ab28ea0..96098e0e2 100644
--- a/src/Wt/Http/Request.C
+++ b/src/Wt/Http/Request.C
@@ -225,8 +225,7 @@ std::string Request::clientAddress() const
     return std::string();
 
   WServer *server = WServer::instance();
-  const bool behindReverseProxy = server && server->configuration().behindReverseProxy();
-  return request_->clientAddress(behindReverseProxy);
+  return request_->clientAddress(server->configuration());
 }
 
 WSslInfo *Request::sslInfo() const
@@ -235,8 +234,7 @@ WSslInfo *Request::sslInfo() const
     return sslInfo_.get();
   if (request_) {
     auto server = WServer::instance();
-    bool behindReverseProxy = server && server->configuration().behindReverseProxy();
-    sslInfo_ = request_->sslInfo(behindReverseProxy);
+    sslInfo_ = request_->sslInfo(server->configuration());
   }
   return sslInfo_.get();
 }
diff --git a/src/Wt/WEnvironment.C b/src/Wt/WEnvironment.C
index 1aef48ddc..1b878896d 100644
--- a/src/Wt/WEnvironment.C
+++ b/src/Wt/WEnvironment.C
@@ -89,7 +89,7 @@ void WEnvironment::updateHostName(const WebRequest& request)
   std::string oldHost = host_;
   host_ = str(request.headerValue("Host"));
 
-  if(conf.behindReverseProxy()) {
+  if (conf.isTrustedProxy(request.remoteAddr())) {
 	std::string forwardedHost = str(request.headerValue("X-Forwarded-Host"));
 
 	if (!forwardedHost.empty()) {
@@ -109,19 +109,16 @@ void WEnvironment::updateUrlScheme(const WebRequest& request)
   urlScheme_       = str(request.urlScheme());
 
   Configuration& conf = session_->controller()->configuration();
-#ifndef WT_TARGET_JAVA
-  if (conf.behindReverseProxy() || server()->dedicatedSessionProcess()) {
-#else
-  if (conf.behindReverseProxy()){
-#endif
-  std::string forwardedProto = str(request.headerValue("X-Forwarded-Proto"));
-  if (!forwardedProto.empty()) {
-	std::string::size_type i = forwardedProto.rfind(',');
-	if (i == std::string::npos)
-	  urlScheme_ = forwardedProto;
-	else
-	  urlScheme_ = forwardedProto.substr(i+1);
-  }
+
+  if (conf.isTrustedProxy(request.remoteAddr())) {
+    std::string forwardedProto = str(request.headerValue("X-Forwarded-Proto"));
+    if (!forwardedProto.empty()) {
+      std::string::size_type i = forwardedProto.rfind(',');
+      if (i == std::string::npos)
+        urlScheme_ = forwardedProto;
+      else
+        urlScheme_ = forwardedProto.substr(i+1);
+    }
   }
 }
 
@@ -144,7 +141,7 @@ void WEnvironment::init(const WebRequest& request)
   if(!str(request.headerValue("Redirect-Secret")).empty())
 	session_->controller()->redirectSecret_ = str(request.headerValue("Redirect-Secret"));
 
-  sslInfo_ = request.sslInfo(conf.behindReverseProxy());
+  sslInfo_ = request.sslInfo(conf);
 #endif
 
   setUserAgent(str(request.headerValue("User-Agent")));
@@ -156,11 +153,7 @@ void WEnvironment::init(const WebRequest& request)
    * If behind a reverse proxy, use external host, schema as communicated using 'X-Forwarded'
    * headers.
    */
-#ifndef WT_TARGET_JAVA
-  if (conf.behindReverseProxy() || server()->dedicatedSessionProcess()) {
-#else
-	if (conf.behindReverseProxy()){
-#endif
+  if (conf.isTrustedProxy(request.remoteAddr())) {
     std::string forwardedHost = str(request.headerValue("X-Forwarded-Host"));
 
     if (!forwardedHost.empty()) {
@@ -183,7 +176,7 @@ void WEnvironment::init(const WebRequest& request)
       host_ += ":" + request.serverPort();
   }
 
-  clientAddress_ = request.clientAddress(conf.behindReverseProxy());
+  clientAddress_ = request.clientAddress(conf);
 
   const char *cookie = request.headerValue("Cookie");
   doesCookies_ = cookie;
diff --git a/src/fcgi/FCGIStream.C b/src/fcgi/FCGIStream.C
index 4e1d03aa7..dee83eb78 100644
--- a/src/fcgi/FCGIStream.C
+++ b/src/fcgi/FCGIStream.C
@@ -257,7 +257,7 @@ namespace {
       return true;
     }
 
-    virtual std::unique_ptr<WSslInfo> sslInfo(bool) const override
+    virtual std::unique_ptr<WSslInfo> sslInfo(const Wt::Configuration &) const override
     {
 #ifdef WT_WITH_SSL
       std::string clientCert = str(envValue("SSL_CLIENT_CERT"));
diff --git a/src/http/HTTPRequest.C b/src/http/HTTPRequest.C
index 6d3fa3ba1..a88bf8c47 100644
--- a/src/http/HTTPRequest.C
+++ b/src/http/HTTPRequest.C
@@ -261,10 +261,10 @@ bool HTTPRequest::isSynchronous() const
   return false;
 }
 
-std::unique_ptr<Wt::WSslInfo> HTTPRequest::sslInfo(bool behindReverseProxy) const
+std::unique_ptr<Wt::WSslInfo> HTTPRequest::sslInfo(const Wt::Configuration &conf) const
 {
   auto result = reply_->request().sslInfo();
-  if (behindReverseProxy) {
+  if (conf.isTrustedProxy(remoteAddr())) {
 #ifdef HTTP_WITH_SSL
     if (!result)
       result = sslInfoFromJson();
diff --git a/src/http/HTTPRequest.h b/src/http/HTTPRequest.h
index 028de43e5..96cf5e89e 100644
--- a/src/http/HTTPRequest.h
+++ b/src/http/HTTPRequest.h
@@ -54,7 +54,7 @@ public:
   virtual const std::string& remoteAddr() const override;
   virtual const char *urlScheme() const override;
   bool isSynchronous() const;
-  virtual std::unique_ptr<Wt::WSslInfo> sslInfo(bool behindReverseProxy) const override;
+  virtual std::unique_ptr<Wt::WSslInfo> sslInfo(const Wt::Configuration & conf) const override;
   virtual const std::vector<std::pair<std::string, std::string> > &urlParams() const override;
 
 private:
diff --git a/src/http/ProxyReply.C b/src/http/ProxyReply.C
index 76463ac09..fc2e95915 100644
--- a/src/http/ProxyReply.C
+++ b/src/http/ProxyReply.C
@@ -255,8 +255,10 @@ void ProxyReply::assembleRequestHeaders()
   std::string forwardedFor;
   std::string forwardedProto = request_.urlScheme;
   std::string forwardedPort;
+  std::string forwardedHost;
   const Wt::Configuration& wtConfiguration
     = connection()->server()->controller()->configuration();
+  const bool trustedProxy = wtConfiguration.isTrustedProxy(request_.remoteIP);
   for (Request::HeaderList::const_iterator it = request_.headers.begin();
        it != request_.headers.end(); ++it) {
     if (it->name.iequals("Connection") || it->name.iequals("Keep-Alive") ||
@@ -269,27 +271,38 @@ void ProxyReply::assembleRequestHeaders()
                  "requests to a child process. Maybe someone is trying to spoof this "
                  "header?");
     } else if (it->name.istarts_with("X-SSL-Client-")) {
-      if (wtConfiguration.behindReverseProxy()) {
+      if (trustedProxy) {
         os << it->name << ": " << it->value << "\r\n";
       } else {
-        LOG_SECURE("wthttp is not behind a reverse proxy, dropping " << it->name.str() << " header");
+        LOG_SECURE("wthttp is not behind a trusted reverse proxy, dropping " << it->name.str() << " header");
       }
-    } else if (it->name.iequals("X-Forwarded-For") ||
-               it->name.iequals("Client-IP")) {
-      if (wtConfiguration.behindReverseProxy()) {
+    } else if (it->name.iequals(wtConfiguration.forwardedHeader().c_str())) {
+      if (trustedProxy) {
         forwardedFor = it->value.str() + ", ";
+      } else {
+        LOG_SECURE("wthttp is not behind a trusted reverse proxy, dropping " << it->name.str() << " header");
       }
     } else if (it->name.iequals("Upgrade")) {
       if (it->value.iequals("websocket")) {
         establishWebSockets = true;
       }
     } else if (it->name.iequals("X-Forwarded-Proto")) { 
-      if (wtConfiguration.behindReverseProxy()) {
+      if (trustedProxy) {
         forwardedProto = it->value.str();
+      } else {
+        LOG_SECURE("wthttp is not behind a trusted reverse proxy, dropping " << it->name.str() << " header");
       }
     } else if(it->name.iequals("X-Forwarded-Port")) {
-      if (wtConfiguration.behindReverseProxy()) {
+      if (trustedProxy) {
         forwardedPort = it->value.str();
+      } else {
+        LOG_SECURE("wthttp is not behind a trusted reverse proxy, dropping " << it->name.str() << " header");
+      }
+    } else if (it->name.iequals("X-Forwarded-Host")) {
+      if (trustedProxy) {
+        forwardedHost = it->value.str();
+      } else {
+        LOG_SECURE("wthttp is not behind a trusted reverse proxy, dropping " << it->name.str() << " header");
       }
     } else if (it->name.length() > 0) {
       os << it->name << ": " << it->value << "\r\n";
@@ -307,6 +320,8 @@ void ProxyReply::assembleRequestHeaders()
     os << "X-Forwarded-Port: " <<  forwardedPort << "\r\n";
   else
     os << "X-Forwarded-Port: " <<  request_.port << "\r\n";
+  if (!forwardedHost.empty())
+    os << "X-Forwarded-Host: " << forwardedHost << "\r\n";
   // Forward SSL Certificate to session only for first request
   if (fwCertificates_) {
     auto sslInfo = request_.sslInfo();
diff --git a/src/http/WServer.C b/src/http/WServer.C
index c334be9bb..1bce9c045 100644
--- a/src/http/WServer.C
+++ b/src/http/WServer.C
@@ -181,6 +181,11 @@ bool WServer::start()
 
   if (impl_->serverConfiguration_->parentPort() != -1) {
     configuration().setBehindReverseProxy(true);
+    configuration().setForwardedHeader("X-Forwarded-For");
+    configuration().setTrustedProxies({
+      Configuration::Network::fromString("127.0.0.1"),
+      Configuration::Network::fromString("::1")
+    });
     dedicatedProcessEnabled_ = true;
   }
 
diff --git a/src/isapi/IsapiRequest.C b/src/isapi/IsapiRequest.C
index 22bbc44d9..00f69ff97 100644
--- a/src/isapi/IsapiRequest.C
+++ b/src/isapi/IsapiRequest.C
@@ -603,7 +603,7 @@ const char *IsapiRequest::urlScheme() const {
     return "http";
 }
 
-std::unique_ptr<WSslInfo> IsapiRequest::sslInfo(bool) const {
+std::unique_ptr<WSslInfo> IsapiRequest::sslInfo(const Configuration &) const {
 #ifdef WT_WITH_SSL
   CERT_CONTEXT_EX cce;
   memset(&cce, 0, sizeof(CERT_CONTEXT_EX));
diff --git a/src/isapi/IsapiRequest.h b/src/isapi/IsapiRequest.h
index 028a4115b..c207422c2 100644
--- a/src/isapi/IsapiRequest.h
+++ b/src/isapi/IsapiRequest.h
@@ -71,7 +71,7 @@ public:
 
   virtual const char *urlScheme() const;
 
-  virtual std::unique_ptr<WSslInfo> sslInfo(bool behindReverseProxy) const;
+  virtual std::unique_ptr<WSslInfo> sslInfo(const Configuration &conf) const;
 
 private:
   LPEXTENSION_CONTROL_BLOCK ecb_;
diff --git a/src/web/Configuration.C b/src/web/Configuration.C
index 75b65b6ac..f6700890b 100644
--- a/src/web/Configuration.C
+++ b/src/web/Configuration.C
@@ -19,12 +19,15 @@
 #include <sys/stat.h>
 #include <fcntl.h>
 #include <algorithm>
+#include <array>
 #include <vector>
 #include <iostream>
 #include <fstream>
 #include <stdlib.h>
 #include <regex>
 
+#include <Wt/AsioWrapper/system_error.hpp>
+
 #include "3rdparty/rapidxml/rapidxml.hpp"
 #include "3rdparty/rapidxml/rapidxml_print.hpp"
 
@@ -157,6 +160,24 @@ std::vector<xml_node<> *> childElements(xml_node<> *element,
   return result;
 }
 
+template<std::size_t N>
+bool prefixMatches(const std::array<unsigned char, N> &networkBytes,
+                   const std::array<unsigned char, N> &addressBytes,
+                   const unsigned char prefixLength)
+{
+  for (std::size_t i = 0; i < N; ++i) {
+    if ((i + 1) * 8 < prefixLength) {
+      if (networkBytes[i] != addressBytes[i]) {
+        return false;
+      }
+    } else {
+      const unsigned char shift = (i + 1) * 8 - prefixLength;
+      return (networkBytes[i] >> shift) == (addressBytes[i] >> shift);
+    }
+  }
+  return true;
+}
+
 static std::string FORWARD_SLASH = "/";
 
 }
@@ -171,6 +192,49 @@ HeadMatter::HeadMatter(std::string contents,
     userAgent_(userAgent)
 { }
 
+Configuration::Network Configuration::Network::fromString(const std::string &s)
+{
+  const auto slashPos = s.find('/');
+  if (slashPos == std::string::npos) {
+    AsioWrapper::error_code ec;
+    const auto address = AsioWrapper::asio::ip::address::from_string(s, ec);
+    if (ec) {
+      throw std::invalid_argument("'" + s + "' is not a valid IP address");
+    }
+    const unsigned char prefixLength = address.is_v6() ? 128 : 32;
+    return Network { address, prefixLength };
+  } else {
+    AsioWrapper::error_code ec;
+    const auto address = AsioWrapper::asio::ip::address::from_string(s.substr(0, slashPos), ec);
+    if (ec) {
+      throw std::invalid_argument("'" + s + "' is not a valid IP address");
+    }
+    const unsigned int prefixLength = Utils::stoi(s.substr(slashPos + 1));
+    if (prefixLength < 0 ||
+        (address.is_v4() && prefixLength > 32) ||
+        (address.is_v6() && prefixLength > 128)) {
+      throw std::invalid_argument("Invalid prefix length " + s.substr(slashPos + 1) + " for IPv" +
+                                  std::string(address.is_v4() ? "4" : "6") + " address");
+    }
+    return Network { address, static_cast<unsigned char>(prefixLength) };
+  }
+}
+
+bool Configuration::Network::contains(const AsioWrapper::asio::ip::address &address) const
+{
+  if (this->address.is_v6() && address.is_v6()) {
+    const auto networkBytes = this->address.to_v6().to_bytes();
+    const auto addressBytes = address.to_v6().to_bytes();
+    return prefixMatches(networkBytes, addressBytes, prefixLength);
+  } else if (this->address.is_v4() && address.is_v4()) {
+    const auto networkBytes = this->address.to_v4().to_bytes();
+    const auto addressBytes = address.to_v4().to_bytes();
+    return prefixMatches(networkBytes, addressBytes, prefixLength);
+  } else {
+    return false;
+  }
+}
+
 Configuration::Configuration(const std::string& applicationPath,
 			     const std::string& appRoot,
 			     const std::string& configurationFile,
@@ -214,6 +278,15 @@ void Configuration::reset()
   properties_.clear();
   xhtmlMimeType_ = false;
   behindReverseProxy_ = false;
+  forwardedHeader_ = "X-Forwarded-For";
+  trustedProxies_ = {
+    Network::fromString("10.0.0.0/8"),
+    Network::fromString("172.16.0.0/12"),
+    Network::fromString("192.168.0.0/16"),
+    Network::fromString("fd00::/8"),
+    Network::fromString("127.0.0.1/32"),
+    Network::fromString("::1/128")
+  };
   redirectMsg_ = "Load basic HTML";
   serializedEvents_ = false;
   webSockets_ = false;
@@ -398,6 +471,31 @@ bool Configuration::behindReverseProxy() const
   return behindReverseProxy_;
 }
 
+std::string Configuration::forwardedHeader() const {
+  READ_LOCK;
+  return forwardedHeader_;
+}
+
+std::vector<Configuration::Network> Configuration::trustedProxies() const {
+  READ_LOCK;
+  return trustedProxies_;
+}
+
+bool Configuration::isTrustedProxy(const std::string &ipAddress) const {
+  READ_LOCK;
+  if (behindReverseProxy_) {
+    return false;
+  }
+  AsioWrapper::error_code ec;
+  const auto address = AsioWrapper::asio::ip::address::from_string(ipAddress, ec);
+  if (ec) {
+    return false;
+  }
+  return std::any_of(begin(trustedProxies_), end(trustedProxies_), [&address](const Network &network) {
+    return network.contains(address);
+  });
+}
+
 std::string Configuration::redirectMessage() const
 {
   READ_LOCK;
@@ -872,6 +970,16 @@ void Configuration::setBehindReverseProxy(bool enabled)
   behindReverseProxy_ = enabled;
 }
 
+void Configuration::setForwardedHeader(const std::string &forwardedHeader)
+{
+  forwardedHeader_ = forwardedHeader;
+}
+
+void Configuration::setTrustedProxies(const std::vector<Network> &trustedProxies)
+{
+  trustedProxies_ = trustedProxies;
+}
+
 void Configuration::readApplicationSettings(xml_node<> *app)
 {
   xml_node<> *sess = singleChildElement(app, "session-management");
@@ -978,6 +1086,24 @@ void Configuration::readApplicationSettings(xml_node<> *app)
   redirectMsg_ = singleChildElementValue(app, "redirect-message", redirectMsg_);
 
   setBoolean(app, "behind-reverse-proxy", behindReverseProxy_);
+
+  forwardedHeader_ = singleChildElementValue(app, "forwarded-header", forwardedHeader_);
+
+  const std::string trustedProxiesStr = singleChildElementValue(app, "trusted-proxies", "");
+  if (!trustedProxiesStr.empty()) {
+    trustedProxies_.clear();
+    std::vector<std::string> splitTrustedProxies;
+    boost::split(splitTrustedProxies, trustedProxiesStr, boost::is_any_of(","));
+    for (auto &trustedProxyStr : splitTrustedProxies) {
+      boost::trim(trustedProxyStr);
+      try {
+        trustedProxies_.push_back(Network::fromString(trustedProxyStr));
+      } catch (const std::invalid_argument &e) {
+        throw WServer::Exception("Invalid <trusted-proxies>: " + std::string(e.what()));
+      }
+    }
+  }
+
   setBoolean(app, "strict-event-serialization", serializedEvents_);
   setBoolean(app, "web-sockets", webSockets_);
 
diff --git a/src/web/Configuration.h b/src/web/Configuration.h
index 718c2ec3f..65eeb24a3 100644
--- a/src/web/Configuration.h
+++ b/src/web/Configuration.h
@@ -42,6 +42,10 @@
 #include "WebSession.h"
 #include "Wt/WRandom.h"
 
+#ifndef WT_TARGET_JAVA
+#include "Wt/AsioWrapper/asio.hpp"
+#endif // WT_TARGET_JAVA
+
 #ifndef WT_TARGET_JAVA
 #include "EntryPoint.h"
 #endif // WT_TARGET_JAVA
@@ -205,9 +209,26 @@ public:
   const std::string *property(const std::string& name) const;
 #endif
 
+#ifndef WT_TARGET_JAVA
+  using IpAddress = AsioWrapper::asio::ip::address;
+#else
+  struct IpAddress {};
+#endif
+
+  struct WT_API Network {
+      IpAddress address;
+      unsigned char prefixLength;
+
+      static Network fromString(const std::string &s);
+      bool contains(const IpAddress &address) const;
+  };
+
   void setAppRoot(const std::string& path);
   std::string appRoot() const;
   bool behindReverseProxy() const;
+  std::string forwardedHeader() const;
+  std::vector<Network> trustedProxies() const;
+  bool isTrustedProxy(const std::string &ipAddress) const;
   std::string redirectMessage() const;
   bool serializedEvents() const;
   bool webSockets() const;
@@ -234,6 +255,8 @@ public:
   void setUseSlashExceptionForInternalPaths(bool enabled);
   void setNeedReadBodyBeforeResponse(bool needed);
   void setBehindReverseProxy(bool enabled);
+  void setForwardedHeader(const std::string &forwardedHeader);
+  void setTrustedProxies(const std::vector<Network> &trustedProxies);
 
   std::string generateSessionId();
   bool registerSessionId(const std::string& oldId, const std::string& newId);
@@ -288,6 +311,8 @@ private:
   PropertyMap     properties_;
   bool            xhtmlMimeType_;
   bool            behindReverseProxy_;
+  std::string     forwardedHeader_;
+  std::vector<Network> trustedProxies_;
   std::string     redirectMsg_;
   bool            serializedEvents_;
   bool		  webSockets_;
diff --git a/src/web/WebRequest.C b/src/web/WebRequest.C
index 679bce600..94cd5fc44 100644
--- a/src/web/WebRequest.C
+++ b/src/web/WebRequest.C
@@ -11,6 +11,7 @@
 
 #include "WebRequest.h"
 #include "WebUtils.h"
+#include "Configuration.h"
 
 #include <cstdlib>
 
@@ -37,24 +38,6 @@ inline std::string str(const char *s) {
   return s ? std::string(s) : std::string();
 }
 
-bool isPrivateIP(const std::string &s) {
-  return boost::starts_with(s, "127.") ||
-         boost::starts_with(s, "10.") ||
-         boost::starts_with(s, "192.168.") ||
-         (s.size() >= 7 &&
-          boost::starts_with(s, "172.") &&
-          s[6] == '.' &&
-          ((s[4] == '1' &&
-            s[5] >= '6' &&
-            s[5] <= '9') ||
-           (s[4] == '2' &&
-            s[5] >= '0' &&
-            s[5] <= '9') ||
-           (s[4] == '3' &&
-            s[5] >= '0' &&
-            s[5] <= '1')));
-}
-
 }
 
 namespace Wt {
@@ -365,46 +348,23 @@ const std::vector<std::pair<std::string, std::string> > &WebRequest::urlParams()
   return urlParams_;
 }
 
-std::string WebRequest::clientAddress(const bool behindReverseProxy) const
+std::string WebRequest::clientAddress(const Configuration &conf) const
 {
-  std::string result;
-
-  /*
-   * Determine client address, taking into account proxies
-   */
-  if (behindReverseProxy) {
-    std::string clientIp = str(headerValue("Client-IP"));
-    boost::trim(clientIp);
-
-    std::vector<std::string> ips;
-    if (!clientIp.empty())
-      boost::split(ips, clientIp, boost::is_any_of(","));
-
-    std::string forwardedFor = str(headerValue("X-Forwarded-For"));
+  std::string remoteAddr = str(envValue("REMOTE_ADDR"));
+  if (conf.isTrustedProxy(remoteAddr)) {
+    std::string forwardedFor = str(headerValue(conf.forwardedHeader().c_str()));
     boost::trim(forwardedFor);
-
     std::vector<std::string> forwardedIps;
-    if (!forwardedFor.empty())
-      boost::split(forwardedIps, forwardedFor, boost::is_any_of(","));
-
-    Utils::insert(ips, forwardedIps);
-
-    for (unsigned i = 0; i < ips.size(); ++i) {
-      result = ips[i];
-
-      boost::trim(result);
-
-      if (!result.empty()
-          && !isPrivateIP(result)) {
-        break;
+    boost::split(forwardedIps, forwardedFor, boost::is_any_of(","));
+    for (auto it = forwardedIps.rbegin();
+         it != forwardedIps.rend(); ++it) {
+      boost::trim(*it);
+      if (!it->empty() && !conf.isTrustedProxy(*it)) {
+        return *it;
       }
     }
   }
-
-  if (result.empty())
-    result = str(envValue("REMOTE_ADDR"));
-
-  return result;
+  return remoteAddr;
 }
 
 }
diff --git a/src/web/WebRequest.h b/src/web/WebRequest.h
index 61864a7c6..35d52c688 100644
--- a/src/web/WebRequest.h
+++ b/src/web/WebRequest.h
@@ -19,6 +19,7 @@
 
 namespace Wt {
 
+class Configuration;
 class EntryPoint;
 class WSslInfo;
 
@@ -193,11 +194,11 @@ public:
    * Returns \c nullptr if the request does not have SSL client certificate
    * information.
    */
-  virtual std::unique_ptr<WSslInfo> sslInfo(bool behindReverseProxy) const = 0;
+  virtual std::unique_ptr<WSslInfo> sslInfo(const Configuration & conf) const = 0;
 
   virtual const std::vector<std::pair<std::string, std::string> >& urlParams() const;
 
-  std::string clientAddress(bool behindReverseProxy) const;
+  std::string clientAddress(const Configuration & conf) const;
 
 protected:
   const EntryPoint *entryPoint_;
diff --git a/src/web/WebSession.C b/src/web/WebSession.C
index e2e33a5a1..84c584742 100644
--- a/src/web/WebSession.C
+++ b/src/web/WebSession.C
@@ -2338,8 +2338,7 @@ void WebSession::notify(const WEvent& event)
 	  return;
 	}
 
-      std::string ca = handler.request()->clientAddress(
-            controller_->configuration().behindReverseProxy());
+      std::string ca = handler.request()->clientAddress(controller_->configuration());
 
       if (ca != env_->clientAddress()) {
 	bool isInvalid = sessionIdCookie_.empty();
diff --git a/src/web/WebSocketMessage.C b/src/web/WebSocketMessage.C
index c031de590..bc7295c7f 100644
--- a/src/web/WebSocketMessage.C
+++ b/src/web/WebSocketMessage.C
@@ -151,9 +151,9 @@ const char *WebSocketMessage::urlScheme() const
     return "http";
 }
 
-std::unique_ptr<Wt::WSslInfo> WebSocketMessage::sslInfo(bool behindReverseProxy) const
+std::unique_ptr<Wt::WSslInfo> WebSocketMessage::sslInfo(const Configuration & conf) const
 {
-  return webSocket()->sslInfo(behindReverseProxy);
+  return webSocket()->sslInfo(conf);
 }
 
 const char *WebSocketMessage::headerValue(const char *name) const
diff --git a/src/web/WebSocketMessage.h b/src/web/WebSocketMessage.h
index 0acc3b83c..e570bf8dc 100644
--- a/src/web/WebSocketMessage.h
+++ b/src/web/WebSocketMessage.h
@@ -47,7 +47,7 @@ public:
 
   virtual const char *urlScheme() const override;
 
-  virtual std::unique_ptr<Wt::WSslInfo> sslInfo(bool behindReverseProxy) const override;
+  virtual std::unique_ptr<Wt::WSslInfo> sslInfo(const Configuration & conf) const override;
 
   virtual const char * headerValue(const char *name) const override;
 
diff --git a/test/http/HttpClientServerTest.C b/test/http/HttpClientServerTest.C
index 084a465f7..fa1d43b26 100644
--- a/test/http/HttpClientServerTest.C
+++ b/test/http/HttpClientServerTest.C
@@ -17,6 +17,8 @@
 #include <Wt/Http/ResponseContinuation.h>
 #include <Wt/Http/Request.h>
 
+#include <web/Configuration.h>
+
 #include <chrono>
 #include <condition_variable>
 #include <mutex>
@@ -34,6 +36,7 @@ namespace {
 	delaySendingBody_(false),
 	haveEverMoreData_(false),
 	haveRandomMoreData_(false),
+	clientAddressTest_(false),
 	aborted_(0)
     { }
 
@@ -57,6 +60,10 @@ namespace {
       haveRandomMoreData_ = true;
     }
 
+    void clientAddressTest() {
+      clientAddressTest_ = true;
+    }
+
     int abortedCount() const {
       return aborted_;
     }
@@ -66,6 +73,8 @@ namespace {
     {
       if (continuation_)
 	handleWithContinuation(request, response);
+      else if (clientAddressTest_)
+        handleClientAddress(request, response);
       else
 	handleSimple(request, response);
     }
@@ -80,6 +89,7 @@ namespace {
     bool delaySendingBody_;
     bool haveEverMoreData_;
     bool haveRandomMoreData_;
+    bool clientAddressTest_;
     int aborted_;
 
     void handleSimple(const Http::Request& request,
@@ -89,6 +99,13 @@ namespace {
       response.out() << "Hello";
     }
 
+    void handleClientAddress(const Http::Request& request,
+                             Http::Response &response)
+    {
+      response.setStatus(200);
+      response.out() << request.clientAddress();
+    }
+
     void handleWithContinuation(const Http::Request& request,
 				Http::Response& response) 
     {
@@ -249,7 +266,8 @@ BOOST_AUTO_TEST_CASE( http_client_server_test3 )
     client.get("http://" + server.address() + "/test");
     client.waitDone();
 
-    BOOST_REQUIRE(client.err() == Wt::AsioWrapper::asio::error::bad_descriptor);
+    BOOST_REQUIRE(client.err() == Wt::AsioWrapper::asio::error::bad_descriptor ||
+                  client.err() == Wt::AsioWrapper::asio::error::operation_aborted);
   }
 
   server.resource().haveMoreData();
@@ -327,6 +345,145 @@ BOOST_AUTO_TEST_CASE( http_client_server_test5 )
   }
 }
 
+// Reserved example IP ranges:
+// - 192.0.2.0/24
+// - 198.51.100.0/24
+// - 203.0.113.0/24
+// - 2001:db8::/32
+
+BOOST_AUTO_TEST_CASE( http_client_address_not_behind_reverse_proxy )
+{
+  Server server;
+  server.resource().clientAddressTest();
+
+  if (server.start()) {
+    Client client;
+
+    std::vector<Http::Message::Header> headers {
+            {"X-Forwarded-For", "198.51.100.1"},
+            {"Client-IP", "203.0.113.1"}
+    };
+
+    client.get("http://" + server.address() + "/test", headers);
+    client.waitDone();
+
+    BOOST_REQUIRE(!client.err());
+    BOOST_REQUIRE(client.message().status() == 200);
+
+    // Client-IP and X-Forwarded-For headers are ignored if not behind reverse proxy
+    BOOST_REQUIRE(client.message().body() == "127.0.0.1");
+  }
+}
+
+BOOST_AUTO_TEST_CASE( http_client_address_forwarded_for )
+{
+  Server server;
+  server.resource().clientAddressTest();
+  server.configuration().setBehindReverseProxy(true);
+
+  if (server.start()) {
+    Client client;
+
+    std::vector<Http::Message::Header> headers {
+            {"X-Forwarded-For", "198.51.100.1"},
+            {"Client-IP", "203.0.113.1"}
+    };
+
+    client.get("http://" + server.address() + "/test", headers);
+    client.waitDone();
+
+    BOOST_REQUIRE(!client.err());
+    BOOST_REQUIRE(client.message().status() == 200);
+
+    // Should get IP address from X-Forwarded-For
+    BOOST_REQUIRE(client.message().body() == "198.51.100.1");
+  }
+}
+
+BOOST_AUTO_TEST_CASE( http_client_address_client_ip )
+{
+  Server server;
+  server.resource().clientAddressTest();
+  server.configuration().setBehindReverseProxy(true);
+  server.configuration().setForwardedHeader("Client-IP");
+
+  if (server.start()) {
+    Client client;
+
+    std::vector<Http::Message::Header> headers {
+            {"X-Forwarded-For", "198.51.100.1"},
+            {"Client-IP", "203.0.113.1"}
+    };
+
+    client.get("http://" + server.address() + "/test", headers);
+    client.waitDone();
+
+    BOOST_REQUIRE(!client.err());
+    BOOST_REQUIRE(client.message().status() == 200);
+
+    // Should get IP address from X-Forwarded-For
+    BOOST_REQUIRE(client.message().body() == "203.0.113.1");
+  }
+}
+
+BOOST_AUTO_TEST_CASE( http_multiple_proxies )
+{
+  Server server;
+  server.resource().clientAddressTest();
+  server.configuration().setBehindReverseProxy(true);
+  server.configuration().setTrustedProxies({
+                                                   Configuration::Network::fromString("127.0.0.1"),
+                                                   Configuration::Network::fromString("198.51.100.0/24")
+                                           });
+
+  if (server.start()) {
+    Client client;
+
+    std::vector<Http::Message::Header> headers {
+            {"X-Forwarded-For", "192.0.2.1, 203.0.113.1, 198.51.100.1"},
+    };
+
+    client.get("http://" + server.address() + "/test", headers);
+    client.waitDone();
+
+    BOOST_REQUIRE(!client.err());
+    BOOST_REQUIRE(client.message().status() == 200);
+
+    // Should get IP address from X-Forwarded-For
+    BOOST_REQUIRE(client.message().body() == "203.0.113.1");
+  }
+}
+
+
+BOOST_AUTO_TEST_CASE( http_multiple_proxies2 )
+{
+  Server server;
+  server.resource().clientAddressTest();
+  server.configuration().setBehindReverseProxy(true);
+  server.configuration().setTrustedProxies({
+                                                   Configuration::Network::fromString("127.0.0.1"),
+                                                   Configuration::Network::fromString("198.51.100.0/24"),
+                                                   Configuration::Network::fromString("203.0.113.0/24")
+                                           });
+
+  if (server.start()) {
+    Client client;
+
+    std::vector<Http::Message::Header> headers {
+            {"X-Forwarded-For", "192.0.2.1, 203.0.113.1, 198.51.100.1"},
+    };
+
+    client.get("http://" + server.address() + "/test", headers);
+    client.waitDone();
+
+    BOOST_REQUIRE(!client.err());
+    BOOST_REQUIRE(client.message().status() == 200);
+
+    // Should get IP address from X-Forwarded-For
+    BOOST_REQUIRE(client.message().body() == "192.0.2.1");
+  }
+}
+
 #endif // WT_THREADED
 
 
diff --git a/test/private/EventDecodeTest.C b/test/private/EventDecodeTest.C
index 9ac3a494e..8ad1c70e5 100644
--- a/test/private/EventDecodeTest.C
+++ b/test/private/EventDecodeTest.C
@@ -117,7 +117,7 @@ public:
     return std::vector<Wt::Http::Message::Header>{};
   }
 
-  virtual std::unique_ptr<Wt::WSslInfo> sslInfo(bool) const override
+  virtual std::unique_ptr<Wt::WSslInfo> sslInfo(const Wt::Configuration &) const override
   {
     return nullptr;
   }
diff --git a/wt_config.xml.in b/wt_config.xml.in
index 817ca88fc..b2dab3c40 100644
--- a/wt_config.xml.in
+++ b/wt_config.xml.in
@@ -404,6 +404,23 @@
 	  -->
 	<behind-reverse-proxy>false</behind-reverse-proxy>
 
+        <!-- Which header to use to get the real client IP address when behind a reverse proxy.
+
+          This could be X-Forwarded-For, CF-Connecting-IP for Cloudflare, True-Client-IP, Fastly-Client-IP,...
+
+          Defaults to X-Forwarded-For
+          -->
+        <forwarded-header>X-Forwarded-For</forwarded-header>
+
+        <!-- Which proxy servers are trusted when behind-reverse-proxy is true.
+
+             You can either enter single IP addresses or IP ranges in CIDR notation,
+             separated by commas. Whitespace is ignored.
+
+             By default, localhost and local IP ranges are trusted.
+          -->
+        <!--<trusted-proxies>10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,fd00::/8,127.0.0.1,::1</trusted-proxies>-->
+
 	<!-- Whether inline CSS is allowed.
 
            Some Wt widgets will insert CSS rules in the the inline
-- 
2.17.1

