From 0532d546d7f3f971cbe9b7f6cf5202995e18d0d6 Mon Sep 17 00:00:00 2001 From: Chocobo1 Date: Sun, 2 Jul 2017 18:23:10 +0800 Subject: [PATCH] Implement HTTP host header filtering This filtering is required to defend against DNS rebinding attack. --- src/base/http/connection.cpp | 4 +- src/base/http/types.h | 4 ++ src/base/preferences.cpp | 10 ++++ src/base/preferences.h | 2 + src/gui/optionsdlg.cpp | 3 ++ src/gui/optionsdlg.ui | 28 ++++++++-- src/webui/abstractwebapplication.cpp | 51 ++++++++++++++++++- src/webui/abstractwebapplication.h | 5 ++ src/webui/prefjson.cpp | 3 ++ src/webui/www/public/preferences_content.html | 7 ++- 10 files changed, 109 insertions(+), 8 deletions(-) diff --git a/src/base/http/connection.cpp b/src/base/http/connection.cpp index 4a8709232..bb63093e6 100644 --- a/src/base/http/connection.cpp +++ b/src/base/http/connection.cpp @@ -74,8 +74,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 4b413420e..9011eb649 100644 --- a/src/base/preferences.cpp +++ b/src/base/preferences.cpp @@ -449,6 +449,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 9ff99216c..118c8edf4 100644 --- a/src/base/preferences.h +++ b/src/base/preferences.h @@ -178,6 +178,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 3513c954a..6bf9829f1 100644 --- a/src/gui/optionsdlg.cpp +++ b/src/gui/optionsdlg.cpp @@ -331,6 +331,7 @@ OptionsDialog::OptionsDialog(QWidget *parent) connect(m_ui->textTrackers, &QPlainTextEdit::textChanged, this, &ThisType::enableApplyButton); #ifndef DISABLE_WEBUI // Web UI tab + connect(m_ui->textSeverDomains, &QLineEdit::textChanged, this, &ThisType::enableApplyButton); connect(m_ui->checkWebUi, &QGroupBox::toggled, this, &ThisType::enableApplyButton); connect(m_ui->spinWebUiPort, qSpinBoxValueChanged, this, &ThisType::enableApplyButton); connect(m_ui->checkWebUIUPnP, &QAbstractButton::toggled, this, &ThisType::enableApplyButton); @@ -626,6 +627,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()); @@ -1013,6 +1015,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 57a2de8e6..e01315667 100644 --- a/src/gui/optionsdlg.ui +++ b/src/gui/optionsdlg.ui @@ -90,7 +90,7 @@ - 1 + 0 @@ -2693,8 +2693,8 @@ 0 0 - 432 - 569 + 518 + 602 @@ -2710,6 +2710,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 a15993b57..ec2d32edb 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, &QTimer::timeout, this, &AbstractWebApplication::removeInactiveSessions); timer->start(60 * 1000); // 1 min. + + connect(Preferences::instance(), &Preferences::changed, this, &AbstractWebApplication::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) @@ -407,6 +417,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; +} + const QStringMap AbstractWebApplication::CONTENT_TYPE_BY_EXT = { { "htm", Http::CONTENT_TYPE_HTML }, { "html", Http::CONTENT_TYPE_HTML }, diff --git a/src/webui/abstractwebapplication.h b/src/webui/abstractwebapplication.h index da4b42780..cfa5a958f 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 8acf605da..befeeb123 100644 --- a/src/webui/prefjson.cpp +++ b/src/webui/prefjson.cpp @@ -162,6 +162,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(); @@ -396,6 +397,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 6edd9fdf4..a9990eb6f 100644 --- a/src/webui/www/public/preferences_content.html +++ b/src/webui/www/public/preferences_content.html @@ -309,7 +309,7 @@ QBT_TR(Share Ratio Limiting)QBT_TR[CONTEXT=OptionsDialog] - @@ -317,7 +317,7 @@ - @@ -406,6 +406,7 @@
QBT_TR(Web User Interface (Remote control))QBT_TR[CONTEXT=OptionsDialog] +


@@ -1049,6 +1050,7 @@ loadPreferences = function() { $('locale_select').setProperty('value', pref.locale); // HTTP Server + $('webui_domain_textarea').setProperty('value', pref.web_ui_domain_list); $('webui_port_value').setProperty('value', pref.web_ui_port); $('webui_upnp_checkbox').setProperty('checked', pref.web_ui_upnp); $('use_https_checkbox').setProperty('checked', pref.use_https); @@ -1316,6 +1318,7 @@ applyPreferences = function() { settings.set('locale', $('locale_select').getProperty('value')); // HTTP Server + settings.set('web_ui_domain_list', $('webui_domain_textarea').getProperty('value')); var web_ui_port = $('webui_port_value').getProperty('value').toInt(); if(isNaN(web_ui_port) || web_ui_port < 1 || web_ui_port > 65535) { alert("QBT_TR(The port used for the Web UI must be between 1 and 65535.)QBT_TR[CONTEXT=HttpServer]");
+
+