Skip to content

Commit

Permalink
Implement HTTP host header filtering
Browse files Browse the repository at this point in the history
This filtering is required to defend against DNS rebinding attack.
  • Loading branch information
Chocobo1 authored and sledgehammer999 committed Jul 12, 2017
1 parent 18651c8 commit 0532d54
Show file tree
Hide file tree
Showing 10 changed files with 109 additions and 8 deletions.
4 changes: 2 additions & 2 deletions src/base/http/connection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
4 changes: 4 additions & 0 deletions src/base/http/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,11 @@ namespace Http

struct Environment
{
QHostAddress localAddress;
quint16 localPort;

QHostAddress clientAddress;
quint16 clientPort;
};

struct UploadedFile
Expand Down
10 changes: 10 additions & 0 deletions src/base/preferences.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 2 additions & 0 deletions src/base/preferences.h
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ class Preferences: public QObject
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;
Expand Down
3 changes: 3 additions & 0 deletions src/gui/optionsdlg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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());
Expand Down
28 changes: 25 additions & 3 deletions src/gui/optionsdlg.ui
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
</widget>
<widget class="QStackedWidget" name="tabOption">
<property name="currentIndex">
<number>1</number>
<number>0</number>
</property>
<widget class="QWidget" name="tabBehaviorPage">
<layout class="QVBoxLayout" name="verticalLayout_10">
Expand Down Expand Up @@ -2693,8 +2693,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>432</width>
<height>569</height>
<width>518</width>
<height>602</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_23">
Expand All @@ -2710,6 +2710,28 @@
<bool>false</bool>
</property>
<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>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
Expand Down
51 changes: 50 additions & 1 deletion src/webui/abstractwebapplication.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@

#include "abstractwebapplication.h"

#include <algorithm>

#include <QCoreApplication>
#include <QDateTime>
#include <QDebug>
Expand Down Expand Up @@ -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()
Expand All @@ -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();
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 },
Expand Down
5 changes: 5 additions & 0 deletions src/webui/abstractwebapplication.h
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ private slots:
void UnbanTimerEvent();
void removeInactiveSessions();

void reloadDomainList();

private:
// Persistent data
QMap<QString, WebSession *> sessions_;
Expand All @@ -97,11 +99,14 @@ private slots:
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);

Expand Down
3 changes: 3 additions & 0 deletions src/webui/prefjson.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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"))
Expand Down
7 changes: 5 additions & 2 deletions src/webui/www/public/preferences_content.html
Original file line number Diff line number Diff line change
Expand Up @@ -309,15 +309,15 @@
<legend>QBT_TR(Share Ratio Limiting)QBT_TR[CONTEXT=OptionsDialog]</legend>
<table>
<tr>
<td>
<td>
<input type="checkbox" id="max_ratio_checkbox" onClick="updateMaxRatioTimeEnabled();"/>
<label for="max_ratio_checkbox">QBT_TR(Seed torrents until their ratio reaches)QBT_TR[CONTEXT=OptionsDialog]</label>
</td>
<td>
<input type="text" id="max_ratio_value" style="width: 4em;"/>
</td>
<tr>
<td>
<td>
<input type="checkbox" id="max_seeding_time_checkbox" onClick="updateMaxRatioTimeEnabled();"/>
<label for="max_seeding_time_checkbox">QBT_TR(Seed torrents until their seeding time reaches)QBT_TR[CONTEXT=OptionsDialog]</label>
</td>
Expand Down Expand Up @@ -406,6 +406,7 @@

<fieldset class="settings">
<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/>
<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/>
Expand Down Expand Up @@ -1049,6 +1050,7 @@
$('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);
Expand Down Expand Up @@ -1316,6 +1318,7 @@
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]");
Expand Down

0 comments on commit 0532d54

Please sign in to comment.