Implement HTTP host header filtering

This filtering is required to defend against DNS rebinding attack.
This commit is contained in:
Chocobo1
2017-07-02 18:23:10 +08:00
committed by sledgehammer999
parent 18651c8d01
commit 0532d546d7
10 changed files with 109 additions and 8 deletions

View File

@@ -28,6 +28,8 @@
#include "abstractwebapplication.h"
#include <algorithm>
#include <QCoreApplication>
#include <QDateTime>
#include <QDebug>
@@ -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 },

View File

@@ -86,6 +86,8 @@ private slots:
void UnbanTimerEvent();
void removeInactiveSessions();
void reloadDomainList();
private:
// Persistent data
QMap<QString, WebSession *> 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);

View File

@@ -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"))

View File

@@ -309,7 +309,7 @@
<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>
@@ -317,7 +317,7 @@
<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>
@@ -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/>
@@ -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]");