Browse Source

Implement HTTP host header filtering

This filtering is required to defend against DNS rebinding attack.
adaptive-webui-19844
Chocobo1 7 years ago committed by sledgehammer999
parent
commit
0532d546d7
No known key found for this signature in database
GPG Key ID: 6E4A2D025B7CC9A2
  1. 4
      src/base/http/connection.cpp
  2. 4
      src/base/http/types.h
  3. 10
      src/base/preferences.cpp
  4. 2
      src/base/preferences.h
  5. 3
      src/gui/optionsdlg.cpp
  6. 28
      src/gui/optionsdlg.ui
  7. 51
      src/webui/abstractwebapplication.cpp
  8. 5
      src/webui/abstractwebapplication.h
  9. 3
      src/webui/prefjson.cpp
  10. 3
      src/webui/www/public/preferences_content.html

4
src/base/http/connection.cpp

@ -74,8 +74,8 @@ void Connection::read()
break; break;
case RequestParser::NoError: case RequestParser::NoError:
Environment env; const Environment env {m_socket->localAddress(), m_socket->localPort(), m_socket->peerAddress(), m_socket->peerPort()};
env.clientAddress = m_socket->peerAddress();
Response response = m_requestHandler->processRequest(request, env); Response response = m_requestHandler->processRequest(request, env);
if (acceptsGzipEncoding(request.headers["accept-encoding"])) if (acceptsGzipEncoding(request.headers["accept-encoding"]))
response.headers[HEADER_CONTENT_ENCODING] = "gzip"; response.headers[HEADER_CONTENT_ENCODING] = "gzip";

4
src/base/http/types.h

@ -65,7 +65,11 @@ namespace Http
struct Environment struct Environment
{ {
QHostAddress localAddress;
quint16 localPort;
QHostAddress clientAddress; QHostAddress clientAddress;
quint16 clientPort;
}; };
struct UploadedFile struct UploadedFile

10
src/base/preferences.cpp

@ -449,6 +449,16 @@ void Preferences::setWebUiLocalAuthEnabled(bool enabled)
setValue("Preferences/WebUI/LocalHostAuth", 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 quint16 Preferences::getWebUiPort() const
{ {
return value("Preferences/WebUI/Port", 8080).toInt(); return value("Preferences/WebUI/Port", 8080).toInt();

2
src/base/preferences.h

@ -178,6 +178,8 @@ public:
void setWebUiEnabled(bool enabled); void setWebUiEnabled(bool enabled);
bool isWebUiLocalAuthEnabled() const; bool isWebUiLocalAuthEnabled() const;
void setWebUiLocalAuthEnabled(bool enabled); void setWebUiLocalAuthEnabled(bool enabled);
QString getServerDomains() const;
void setServerDomains(const QString &str);
quint16 getWebUiPort() const; quint16 getWebUiPort() const;
void setWebUiPort(quint16 port); void setWebUiPort(quint16 port);
bool useUPnPForWebUIPort() const; bool useUPnPForWebUIPort() const;

3
src/gui/optionsdlg.cpp

@ -331,6 +331,7 @@ OptionsDialog::OptionsDialog(QWidget *parent)
connect(m_ui->textTrackers, &QPlainTextEdit::textChanged, this, &ThisType::enableApplyButton); connect(m_ui->textTrackers, &QPlainTextEdit::textChanged, this, &ThisType::enableApplyButton);
#ifndef DISABLE_WEBUI #ifndef DISABLE_WEBUI
// Web UI tab // Web UI tab
connect(m_ui->textSeverDomains, &QLineEdit::textChanged, this, &ThisType::enableApplyButton);
connect(m_ui->checkWebUi, &QGroupBox::toggled, this, &ThisType::enableApplyButton); connect(m_ui->checkWebUi, &QGroupBox::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->spinWebUiPort, qSpinBoxValueChanged, this, &ThisType::enableApplyButton); connect(m_ui->spinWebUiPort, qSpinBoxValueChanged, this, &ThisType::enableApplyButton);
connect(m_ui->checkWebUIUPnP, &QAbstractButton::toggled, this, &ThisType::enableApplyButton); connect(m_ui->checkWebUIUPnP, &QAbstractButton::toggled, this, &ThisType::enableApplyButton);
@ -626,6 +627,7 @@ void OptionsDialog::saveOptions()
// Web UI // Web UI
pref->setWebUiEnabled(isWebUiEnabled()); pref->setWebUiEnabled(isWebUiEnabled());
if (isWebUiEnabled()) { if (isWebUiEnabled()) {
pref->setServerDomains(m_ui->textSeverDomains->text());
pref->setWebUiPort(webUiPort()); pref->setWebUiPort(webUiPort());
pref->setUPnPForWebUIPort(m_ui->checkWebUIUPnP->isChecked()); pref->setUPnPForWebUIPort(m_ui->checkWebUIUPnP->isChecked());
pref->setWebUiHttpsEnabled(m_ui->checkWebUiHttps->isChecked()); pref->setWebUiHttpsEnabled(m_ui->checkWebUiHttps->isChecked());
@ -1013,6 +1015,7 @@ void OptionsDialog::loadOptions()
// End Bittorrent preferences // End Bittorrent preferences
// Web UI preferences // Web UI preferences
m_ui->textSeverDomains->setText(pref->getServerDomains());
m_ui->checkWebUi->setChecked(pref->isWebUiEnabled()); m_ui->checkWebUi->setChecked(pref->isWebUiEnabled());
m_ui->spinWebUiPort->setValue(pref->getWebUiPort()); m_ui->spinWebUiPort->setValue(pref->getWebUiPort());
m_ui->checkWebUIUPnP->setChecked(pref->useUPnPForWebUIPort()); m_ui->checkWebUIUPnP->setChecked(pref->useUPnPForWebUIPort());

28
src/gui/optionsdlg.ui

@ -90,7 +90,7 @@
</widget> </widget>
<widget class="QStackedWidget" name="tabOption"> <widget class="QStackedWidget" name="tabOption">
<property name="currentIndex"> <property name="currentIndex">
<number>1</number> <number>0</number>
</property> </property>
<widget class="QWidget" name="tabBehaviorPage"> <widget class="QWidget" name="tabBehaviorPage">
<layout class="QVBoxLayout" name="verticalLayout_10"> <layout class="QVBoxLayout" name="verticalLayout_10">
@ -2693,8 +2693,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>432</width> <width>518</width>
<height>569</height> <height>602</height>
</rect> </rect>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_23"> <layout class="QVBoxLayout" name="verticalLayout_23">
@ -2710,6 +2710,28 @@
<bool>false</bool> <bool>false</bool>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_2"> <layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_10">
<item>
<widget class="QLabel" name="labelServerDomains">
<property name="text">
<string>Server domains:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="textSeverDomains">
<property name="toolTip">
<string>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 '*'.</string>
</property>
</widget>
</item>
</layout>
</item>
<item> <item>
<layout class="QHBoxLayout" name="horizontalLayout_2"> <layout class="QHBoxLayout" name="horizontalLayout_2">
<item> <item>

51
src/webui/abstractwebapplication.cpp

@ -28,6 +28,8 @@
#include "abstractwebapplication.h" #include "abstractwebapplication.h"
#include <algorithm>
#include <QCoreApplication> #include <QCoreApplication>
#include <QDateTime> #include <QDateTime>
#include <QDebug> #include <QDebug>
@ -91,6 +93,8 @@ AbstractWebApplication::AbstractWebApplication(QObject *parent)
QTimer *timer = new QTimer(this); QTimer *timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, &AbstractWebApplication::removeInactiveSessions); connect(timer, &QTimer::timeout, this, &AbstractWebApplication::removeInactiveSessions);
timer->start(60 * 1000); // 1 min. timer->start(60 * 1000); // 1 min.
connect(Preferences::instance(), &Preferences::changed, this, &AbstractWebApplication::reloadDomainList);
} }
AbstractWebApplication::~AbstractWebApplication() 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';"); 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 // block cross-site requests
if (isCrossSiteRequest(request_)) { if (isCrossSiteRequest(request_) || !validateHostHeader(request_, env, domainList)) {
status(401, "Unauthorized"); status(401, "Unauthorized");
return response(); 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() bool AbstractWebApplication::sessionInitialize()
{ {
if (session_ == 0) if (session_ == 0)
@ -407,6 +417,45 @@ bool AbstractWebApplication::isCrossSiteRequest(const Http::Request &request) co
return true; 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 = { const QStringMap AbstractWebApplication::CONTENT_TYPE_BY_EXT = {
{ "htm", Http::CONTENT_TYPE_HTML }, { "htm", Http::CONTENT_TYPE_HTML },
{ "html", Http::CONTENT_TYPE_HTML }, { "html", Http::CONTENT_TYPE_HTML },

5
src/webui/abstractwebapplication.h

@ -86,6 +86,8 @@ private slots:
void UnbanTimerEvent(); void UnbanTimerEvent();
void removeInactiveSessions(); void removeInactiveSessions();
void reloadDomainList();
private: private:
// Persistent data // Persistent data
QMap<QString, WebSession *> sessions_; QMap<QString, WebSession *> sessions_;
@ -97,11 +99,14 @@ private:
Http::Request request_; Http::Request request_;
Http::Environment env_; Http::Environment env_;
QStringList domainList;
QString generateSid(); QString generateSid();
bool sessionInitialize(); bool sessionInitialize();
QStringMap parseCookie(const Http::Request &request) const; QStringMap parseCookie(const Http::Request &request) const;
bool isCrossSiteRequest(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); static void translateDocument(QString &data);

3
src/webui/prefjson.cpp

@ -162,6 +162,7 @@ QByteArray prefjson::getPreferences()
// Language // Language
data["locale"] = pref->getLocale(); data["locale"] = pref->getLocale();
// HTTP Server // HTTP Server
data["web_ui_domain_list"] = pref->getServerDomains();
data["web_ui_port"] = pref->getWebUiPort(); data["web_ui_port"] = pref->getWebUiPort();
data["web_ui_upnp"] = pref->useUPnPForWebUIPort(); data["web_ui_upnp"] = pref->useUPnPForWebUIPort();
data["use_https"] = pref->isWebUiHttpsEnabled(); data["use_https"] = pref->isWebUiHttpsEnabled();
@ -396,6 +397,8 @@ void prefjson::setPreferences(const QString& json)
} }
} }
// HTTP Server // HTTP Server
if (m.contains("web_ui_domain_list"))
pref->setServerDomains(m["web_ui_domain_list"].toString());
if (m.contains("web_ui_port")) if (m.contains("web_ui_port"))
pref->setWebUiPort(m["web_ui_port"].toUInt()); pref->setWebUiPort(m["web_ui_port"].toUInt());
if (m.contains("web_ui_upnp")) if (m.contains("web_ui_upnp"))

3
src/webui/www/public/preferences_content.html

@ -406,6 +406,7 @@
<fieldset class="settings"> <fieldset class="settings">
<legend>QBT_TR(Web User Interface (Remote control))QBT_TR[CONTEXT=OptionsDialog]</legend> <legend>QBT_TR(Web User Interface (Remote control))QBT_TR[CONTEXT=OptionsDialog]</legend>
<label for="webui_domain_textarea">QBT_TR(Server domains:)QBT_TR[CONTEXT=OptionsDialog]</label><textarea id="webui_domain_textarea" rows="1" cols="70"></textarea><br/>
<label for="webui_port_value">QBT_TR(Port:)QBT_TR[CONTEXT=OptionsDialog]</label><input type="text" id="webui_port_value" style="width: 4em;"/><br/> <label for="webui_port_value">QBT_TR(Port:)QBT_TR[CONTEXT=OptionsDialog]</label><input type="text" id="webui_port_value" style="width: 4em;"/><br/>
<input type="checkbox" id="webui_upnp_checkbox"/> <input type="checkbox" id="webui_upnp_checkbox"/>
<label for="webui_upnp_checkbox">QBT_TR(Use UPnP / NAT-PMP to forward the port from my router)QBT_TR[CONTEXT=OptionsDialog]</label><br/> <label for="webui_upnp_checkbox">QBT_TR(Use UPnP / NAT-PMP to forward the port from my router)QBT_TR[CONTEXT=OptionsDialog]</label><br/>
@ -1049,6 +1050,7 @@ loadPreferences = function() {
$('locale_select').setProperty('value', pref.locale); $('locale_select').setProperty('value', pref.locale);
// HTTP Server // HTTP Server
$('webui_domain_textarea').setProperty('value', pref.web_ui_domain_list);
$('webui_port_value').setProperty('value', pref.web_ui_port); $('webui_port_value').setProperty('value', pref.web_ui_port);
$('webui_upnp_checkbox').setProperty('checked', pref.web_ui_upnp); $('webui_upnp_checkbox').setProperty('checked', pref.web_ui_upnp);
$('use_https_checkbox').setProperty('checked', pref.use_https); $('use_https_checkbox').setProperty('checked', pref.use_https);
@ -1316,6 +1318,7 @@ applyPreferences = function() {
settings.set('locale', $('locale_select').getProperty('value')); settings.set('locale', $('locale_select').getProperty('value'));
// HTTP Server // HTTP Server
settings.set('web_ui_domain_list', $('webui_domain_textarea').getProperty('value'));
var web_ui_port = $('webui_port_value').getProperty('value').toInt(); var web_ui_port = $('webui_port_value').getProperty('value').toInt();
if(isNaN(web_ui_port) || web_ui_port < 1 || web_ui_port > 65535) { 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]"); alert("QBT_TR(The port used for the Web UI must be between 1 and 65535.)QBT_TR[CONTEXT=HttpServer]");

Loading…
Cancel
Save