diff --git a/src/base/http/connection.cpp b/src/base/http/connection.cpp
index 46dadd22b..a6e919a1b 100644
--- a/src/base/http/connection.cpp
+++ b/src/base/http/connection.cpp
@@ -77,8 +77,8 @@ void Connection::read()
break;
case RequestParser::NoError:
- Environment env;
- env.clientAddress = m_socket->peerAddress();
+ const Environment env {m_socket->localAddress(), m_socket->localPort(), m_socket->peerAddress(), m_socket->peerPort()};
+
Response response = m_requestHandler->processRequest(request, env);
if (acceptsGzipEncoding(request.headers["accept-encoding"]))
response.headers[HEADER_CONTENT_ENCODING] = "gzip";
diff --git a/src/base/http/types.h b/src/base/http/types.h
index cc86e79d6..488edf9e3 100644
--- a/src/base/http/types.h
+++ b/src/base/http/types.h
@@ -65,7 +65,11 @@ namespace Http
struct Environment
{
+ QHostAddress localAddress;
+ quint16 localPort;
+
QHostAddress clientAddress;
+ quint16 clientPort;
};
struct UploadedFile
diff --git a/src/base/preferences.cpp b/src/base/preferences.cpp
index 618c259bf..e49150ed3 100644
--- a/src/base/preferences.cpp
+++ b/src/base/preferences.cpp
@@ -439,6 +439,16 @@ void Preferences::setWebUiLocalAuthEnabled(bool enabled)
setValue("Preferences/WebUI/LocalHostAuth", enabled);
}
+QString Preferences::getServerDomains() const
+{
+ return value("Preferences/WebUI/ServerDomains", "*").toString();
+}
+
+void Preferences::setServerDomains(const QString &str)
+{
+ setValue("Preferences/WebUI/ServerDomains", str);
+}
+
quint16 Preferences::getWebUiPort() const
{
return value("Preferences/WebUI/Port", 8080).toInt();
diff --git a/src/base/preferences.h b/src/base/preferences.h
index ec328f264..d6abf8e24 100644
--- a/src/base/preferences.h
+++ b/src/base/preferences.h
@@ -175,6 +175,8 @@ public:
void setWebUiEnabled(bool enabled);
bool isWebUiLocalAuthEnabled() const;
void setWebUiLocalAuthEnabled(bool enabled);
+ QString getServerDomains() const;
+ void setServerDomains(const QString &str);
quint16 getWebUiPort() const;
void setWebUiPort(quint16 port);
bool useUPnPForWebUIPort() const;
diff --git a/src/gui/optionsdlg.cpp b/src/gui/optionsdlg.cpp
index 2e5d5711f..b227ea62f 100644
--- a/src/gui/optionsdlg.cpp
+++ b/src/gui/optionsdlg.cpp
@@ -319,6 +319,7 @@ OptionsDialog::OptionsDialog(QWidget *parent)
connect(m_ui->textTrackers, SIGNAL(textChanged()), this, SLOT(enableApplyButton()));
#ifndef DISABLE_WEBUI
// Web UI tab
+ connect(m_ui->textSeverDomains, SIGNAL(textChanged(QString)), this, SLOT(enableApplyButton()));
connect(m_ui->checkWebUi, SIGNAL(toggled(bool)), this, SLOT(enableApplyButton()));
connect(m_ui->spinWebUiPort, SIGNAL(valueChanged(int)), this, SLOT(enableApplyButton()));
connect(m_ui->checkWebUIUPnP, SIGNAL(toggled(bool)), SLOT(enableApplyButton()));
@@ -603,6 +604,7 @@ void OptionsDialog::saveOptions()
// Web UI
pref->setWebUiEnabled(isWebUiEnabled());
if (isWebUiEnabled()) {
+ pref->setServerDomains(m_ui->textSeverDomains->text());
pref->setWebUiPort(webUiPort());
pref->setUPnPForWebUIPort(m_ui->checkWebUIUPnP->isChecked());
pref->setWebUiHttpsEnabled(m_ui->checkWebUiHttps->isChecked());
@@ -975,6 +977,7 @@ void OptionsDialog::loadOptions()
// End Bittorrent preferences
// Web UI preferences
+ m_ui->textSeverDomains->setText(pref->getServerDomains());
m_ui->checkWebUi->setChecked(pref->isWebUiEnabled());
m_ui->spinWebUiPort->setValue(pref->getWebUiPort());
m_ui->checkWebUIUPnP->setChecked(pref->useUPnPForWebUIPort());
diff --git a/src/gui/optionsdlg.ui b/src/gui/optionsdlg.ui
index 46250286e..f286046fd 100644
--- a/src/gui/optionsdlg.ui
+++ b/src/gui/optionsdlg.ui
@@ -2605,8 +2605,8 @@
0
0
- 438
- 543
+ 518
+ 602
@@ -2622,6 +2622,28 @@
false
+ -
+
+
-
+
+
+ Server domains:
+
+
+
+ -
+
+
+ Whitelist for filtering HTTP Host header values.
+In order to defend against DNS rebinding attack,
+you should put in domain names used by WebUI server.
+
+Use ';' to split multiple entries. Can use wildcard '*'.
+
+
+
+
+
-
-
diff --git a/src/webui/abstractwebapplication.cpp b/src/webui/abstractwebapplication.cpp
index 613013825..638ef9d78 100644
--- a/src/webui/abstractwebapplication.cpp
+++ b/src/webui/abstractwebapplication.cpp
@@ -28,6 +28,8 @@
#include "abstractwebapplication.h"
+#include
+
#include
#include
#include
@@ -91,6 +93,8 @@ AbstractWebApplication::AbstractWebApplication(QObject *parent)
QTimer *timer = new QTimer(this);
connect(timer, SIGNAL(timeout()), SLOT(removeInactiveSessions()));
timer->start(60 * 1000); // 1 min.
+
+ connect(Preferences::instance(), SIGNAL(changed()), this, SLOT(reloadDomainList()));
}
AbstractWebApplication::~AbstractWebApplication()
@@ -115,7 +119,7 @@ Http::Response AbstractWebApplication::processRequest(const Http::Request &reque
header(Http::HEADER_CONTENT_SECURITY_POLICY, "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; script-src 'self' 'unsafe-inline'; object-src 'none';");
// block cross-site requests
- if (isCrossSiteRequest(request_)) {
+ if (isCrossSiteRequest(request_) || !validateHostHeader(request_, env, domainList)) {
status(401, "Unauthorized");
return response();
}
@@ -153,6 +157,12 @@ void AbstractWebApplication::removeInactiveSessions()
}
}
+void AbstractWebApplication::reloadDomainList()
+{
+ domainList = Preferences::instance()->getServerDomains().split(';', QString::SkipEmptyParts);
+ std::for_each(domainList.begin(), domainList.end(), [](QString &entry){ entry = entry.trimmed(); });
+}
+
bool AbstractWebApplication::sessionInitialize()
{
if (session_ == 0)
@@ -411,6 +421,45 @@ bool AbstractWebApplication::isCrossSiteRequest(const Http::Request &request) co
return true;
}
+bool AbstractWebApplication::validateHostHeader(const Http::Request &request, const Http::Environment &env, const QStringList &domains) const
+{
+ const QUrl hostHeader = QUrl::fromUserInput(
+ request.headers.value(Http::HEADER_X_FORWARDED_HOST, request.headers.value(Http::HEADER_HOST)));
+
+ // (if present) try matching host header's port with local port
+ const int requestPort = hostHeader.port();
+ if ((requestPort != -1) && (env.localPort != requestPort))
+ return false;
+
+ // try matching host header with local address
+ const QString requestHost = hostHeader.host();
+
+#if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0))
+ const bool sameAddr = env.localAddress.isEqual(QHostAddress(requestHost));
+#else
+ const auto equal = [](const Q_IPV6ADDR &l, const Q_IPV6ADDR &r) -> bool {
+ for (int i = 0; i < 16; ++i) {
+ if (l[i] != r[i])
+ return false;
+ }
+ return true;
+ };
+ const bool sameAddr = equal(env.localAddress.toIPv6Address(), QHostAddress(requestHost).toIPv6Address());
+#endif
+
+ if (sameAddr)
+ return true;
+
+ // try matching host header with domain list
+ for (const auto &domain : domains) {
+ QRegExp domainRegex(domain, Qt::CaseInsensitive, QRegExp::Wildcard);
+ if (requestHost.contains(domainRegex))
+ return true;
+ }
+
+ return false;
+}
+
QStringMap AbstractWebApplication::initializeContentTypeByExtMap()
{
QStringMap map;
diff --git a/src/webui/abstractwebapplication.h b/src/webui/abstractwebapplication.h
index ed4492934..c723e0520 100644
--- a/src/webui/abstractwebapplication.h
+++ b/src/webui/abstractwebapplication.h
@@ -86,6 +86,8 @@ private slots:
void UnbanTimerEvent();
void removeInactiveSessions();
+ void reloadDomainList();
+
private:
// Persistent data
QMap sessions_;
@@ -97,11 +99,14 @@ private:
Http::Request request_;
Http::Environment env_;
+ QStringList domainList;
+
QString generateSid();
bool sessionInitialize();
QStringMap parseCookie(const Http::Request &request) const;
bool isCrossSiteRequest(const Http::Request &request) const;
+ bool validateHostHeader(const Http::Request &request, const Http::Environment &env, const QStringList &domains) const;
static void translateDocument(QString &data);
diff --git a/src/webui/prefjson.cpp b/src/webui/prefjson.cpp
index ee26028b3..86a70a512 100644
--- a/src/webui/prefjson.cpp
+++ b/src/webui/prefjson.cpp
@@ -159,6 +159,7 @@ QByteArray prefjson::getPreferences()
// Language
data["locale"] = pref->getLocale();
// HTTP Server
+ data["web_ui_domain_list"] = pref->getServerDomains();
data["web_ui_port"] = pref->getWebUiPort();
data["web_ui_upnp"] = pref->useUPnPForWebUIPort();
data["use_https"] = pref->isWebUiHttpsEnabled();
@@ -387,6 +388,8 @@ void prefjson::setPreferences(const QString& json)
}
}
// HTTP Server
+ if (m.contains("web_ui_domain_list"))
+ pref->setServerDomains(m["web_ui_domain_list"].toString());
if (m.contains("web_ui_port"))
pref->setWebUiPort(m["web_ui_port"].toUInt());
if (m.contains("web_ui_upnp"))
diff --git a/src/webui/www/public/preferences_content.html b/src/webui/www/public/preferences_content.html
index 50a6d2343..f3c1ce54c 100644
--- a/src/webui/www/public/preferences_content.html
+++ b/src/webui/www/public/preferences_content.html
@@ -378,6 +378,7 @@