mirror of
https://github.com/d47081/qBittorrent.git
synced 2025-01-11 23:37:59 +00:00
Implement HTTP host header filtering
This filtering is required to defend against DNS rebinding attack.
This commit is contained in:
parent
18651c8d01
commit
0532d546d7
@ -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";
|
||||||
|
@ -65,7 +65,11 @@ namespace Http
|
|||||||
|
|
||||||
struct Environment
|
struct Environment
|
||||||
{
|
{
|
||||||
|
QHostAddress localAddress;
|
||||||
|
quint16 localPort;
|
||||||
|
|
||||||
QHostAddress clientAddress;
|
QHostAddress clientAddress;
|
||||||
|
quint16 clientPort;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct UploadedFile
|
struct UploadedFile
|
||||||
|
@ -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();
|
||||||
|
@ -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;
|
||||||
|
@ -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());
|
||||||
|
@ -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>
|
||||||
|
@ -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 },
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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"))
|
||||||
|
@ -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…
Reference in New Issue
Block a user