diff --git a/defaults/application.ini b/defaults/application.ini index f4ad98e75..712f0995b 100644 --- a/defaults/application.ini +++ b/defaults/application.ini @@ -83,8 +83,14 @@ EnableHttpMethodOverride=false # the client, if true. EnableForwardedForHeader=false -# Specify IP addresses of the proxy servers to work the feature of -# X-Forwarded-For header. +# If false is specified, only the right-most IP address in X-Forwarded-For +# request header is parsed, otherwise all IP addresses in header are parsed +# from right side to left stopping at the last non-trusted IP address. +ParseForwardedForHeaderRecursively=false + +# Specify trusted proxy servers to work the feature of X-Forwarded-For header. +# Set to a quoted comma-delimited list of IP addresses or subnets, +# for example: "192.168.1.10, 10.0.1.0/24, 10.0.2.0/255.255.255.0" TrustedProxyServers= # Sets the timeout in seconds during which a keep-alive HTTP connection diff --git a/src/tappsettings.cpp b/src/tappsettings.cpp index 148f9f191..ebf6c1b19 100644 --- a/src/tappsettings.cpp +++ b/src/tappsettings.cpp @@ -41,6 +41,7 @@ class AttributeMap : public QMap { insert(Tf::EnableCsrfProtectionModule, "EnableCsrfProtectionModule"); insert(Tf::EnableHttpMethodOverride, "EnableHttpMethodOverride"); insert(Tf::EnableForwardedForHeader, "EnableForwardedForHeader"); + insert(Tf::ParseForwardedForHeaderRecursively, "ParseForwardedForHeaderRecursively"); insert(Tf::TrustedProxyServers, "TrustedProxyServers"); insert(Tf::HttpKeepAliveTimeout, "HttpKeepAliveTimeout"); insert(Tf::LDPreload, "LDPreload"); diff --git a/src/tfnamespace.h b/src/tfnamespace.h index c464911c6..8f7702143 100644 --- a/src/tfnamespace.h +++ b/src/tfnamespace.h @@ -203,6 +203,7 @@ enum AppAttribute { SessionCookieSameSite, // EnableForwardedForHeader, + ParseForwardedForHeaderRecursively, TrustedProxyServers, }; diff --git a/src/thttprequest.cpp b/src/thttprequest.cpp index 262e1f092..f5f251518 100644 --- a/src/thttprequest.cpp +++ b/src/thttprequest.cpp @@ -602,6 +602,18 @@ QList THttpRequest::generate(const QByteArray &byteArray, const QH return reqList; } +typedef QPair NetworkSubnet; + +static bool isInAnySubnet(const QHostAddress &ip, const QList &subnets) +{ + for (const auto &subnet : subnets) { + if (ip.isInSubnet(subnet.first, subnet.second)) { + return true; + } + } + return false; +} + /*! Returns a originating IP address of the client by parsing the 'X-Forwarded-For' header of the request. To enable this feature, edit application.ini and @@ -611,44 +623,44 @@ QList THttpRequest::generate(const QByteArray &byteArray, const QH QHostAddress THttpRequest::originatingClientAddress() const { static const bool EnableForwardedForHeader = Tf::appSettings()->value(Tf::EnableForwardedForHeader, false).toBool(); - static const QStringList TrustedProxyServers = []() { // delimiter: comma or space - QStringList servers; - for (auto &s : Tf::appSettings()->value(Tf::TrustedProxyServers).toStringList()) { - servers << s.simplified().split(QLatin1Char(' ')); - } - - QHostAddress ip; - for (QMutableListIterator it(servers); it.hasNext();) { - auto &s = it.next(); - if (!ip.setAddress(s)) { // check IP address - it.remove(); + static const bool ParseForwardedForHeaderRecursively = Tf::appSettings()->value(Tf::ParseForwardedForHeaderRecursively, false).toBool(); + static const bool ListeningOnUnixDomainSocket = Tf::appSettings()->value(Tf::ListenPort).toString().trimmed().startsWith(QStringLiteral("unix:")); + static const bool TrustUnixDomainSocketProxy = true; + static const QList TrustProxyServersInSubnets = []() { + QList subnets; + for (const QString &s : Tf::appSettings()->value(Tf::TrustedProxyServers).toString().split(QRegularExpression(QStringLiteral("\\s*,\\s*")), QString::SkipEmptyParts)) { + NetworkSubnet subnet = QHostAddress::parseSubnet(s); + if (subnet.first.protocol() != QAbstractSocket::UnknownNetworkLayerProtocol) { + subnets.append(subnet); + } else { + tSystemWarn("Invalid IP address or subnet '%s' in TrustedProxyServers parameter", qPrintable(s)); } } - return servers; + return subnets; }(); - QString remoteHost; - if (EnableForwardedForHeader) { - if (TrustedProxyServers.isEmpty()) { - T_ONCE(tWarn("TrustedProxyServers parameter of config is empty!")); - } + QHostAddress remoteAddress = clientAddress(); - auto hosts = QString::fromLatin1(header().rawHeader(QByteArrayLiteral("X-Forwarded-For"))).simplified().split(QRegularExpression("\\s?,\\s?"), QString::SkipEmptyParts); - if (hosts.isEmpty()) { - tWarn("'X-Forwarded-For' header is empty"); - } else { - for (auto &proxy : TrustedProxyServers) { - hosts.removeAll(proxy); - } - - if (!hosts.isEmpty()) { - remoteHost = hosts.last(); + if (EnableForwardedForHeader) { + const QByteArray forwardedForHeader = header().rawHeader(QByteArrayLiteral("X-Forwarded-For")); + if (!forwardedForHeader.isEmpty()) { + QStringList hosts = QString::fromLatin1(forwardedForHeader).trimmed().split(QRegularExpression(QStringLiteral("\\s*,\\s*"))); + + bool trustProxy = (ListeningOnUnixDomainSocket && TrustUnixDomainSocketProxy) || isInAnySubnet(remoteAddress, TrustProxyServersInSubnets); + while (trustProxy && !hosts.isEmpty()) { + QHostAddress host; + if (!host.setAddress(hosts.takeLast())) + break; + remoteAddress = host; + if (!ParseForwardedForHeaderRecursively) + break; + trustProxy = isInAnySubnet(remoteAddress, TrustProxyServersInSubnets); } } } - return (remoteHost.isEmpty()) ? clientAddress() : QHostAddress(remoteHost); -} + return remoteAddress; +} QIODevice *THttpRequest::rawBody() {