mirror of
https://github.com/d47081/qBittorrent.git
synced 2025-01-25 14:04:23 +00:00
Merge pull request #8967 from Chocobo1/protect
Add options to control WebUI security measures
This commit is contained in:
commit
4a51f14328
@ -576,6 +576,26 @@ void Preferences::setWebUiPassword(const QString &new_password)
|
|||||||
setValue("Preferences/WebUI/Password_ha1", md5.result().toHex());
|
setValue("Preferences/WebUI/Password_ha1", md5.result().toHex());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Preferences::isWebUiClickjackingProtectionEnabled() const
|
||||||
|
{
|
||||||
|
return value("Preferences/WebUI/ClickjackingProtection", true).toBool();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Preferences::setWebUiClickjackingProtectionEnabled(bool enabled)
|
||||||
|
{
|
||||||
|
setValue("Preferences/WebUI/ClickjackingProtection", enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Preferences::isWebUiCSRFProtectionEnabled() const
|
||||||
|
{
|
||||||
|
return value("Preferences/WebUI/CSRFProtection", true).toBool();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Preferences::setWebUiCSRFProtectionEnabled(bool enabled)
|
||||||
|
{
|
||||||
|
setValue("Preferences/WebUI/CSRFProtection", enabled);
|
||||||
|
}
|
||||||
|
|
||||||
bool Preferences::isWebUiHttpsEnabled() const
|
bool Preferences::isWebUiHttpsEnabled() const
|
||||||
{
|
{
|
||||||
return value("Preferences/WebUI/HTTPS/Enabled", false).toBool();
|
return value("Preferences/WebUI/HTTPS/Enabled", false).toBool();
|
||||||
|
@ -194,6 +194,12 @@ public:
|
|||||||
QString getWebUiPassword() const;
|
QString getWebUiPassword() const;
|
||||||
void setWebUiPassword(const QString &new_password);
|
void setWebUiPassword(const QString &new_password);
|
||||||
|
|
||||||
|
// WebUI security
|
||||||
|
bool isWebUiClickjackingProtectionEnabled() const;
|
||||||
|
void setWebUiClickjackingProtectionEnabled(bool enabled);
|
||||||
|
bool isWebUiCSRFProtectionEnabled() const;
|
||||||
|
void setWebUiCSRFProtectionEnabled(bool enabled);
|
||||||
|
|
||||||
// HTTPS
|
// HTTPS
|
||||||
bool isWebUiHttpsEnabled() const;
|
bool isWebUiHttpsEnabled() const;
|
||||||
void setWebUiHttpsEnabled(bool enabled);
|
void setWebUiHttpsEnabled(bool enabled);
|
||||||
|
@ -378,6 +378,8 @@ OptionsDialog::OptionsDialog(QWidget *parent)
|
|||||||
connect(m_ui->checkBypassLocalAuth, &QAbstractButton::toggled, this, &ThisType::enableApplyButton);
|
connect(m_ui->checkBypassLocalAuth, &QAbstractButton::toggled, this, &ThisType::enableApplyButton);
|
||||||
connect(m_ui->checkBypassAuthSubnetWhitelist, &QAbstractButton::toggled, this, &ThisType::enableApplyButton);
|
connect(m_ui->checkBypassAuthSubnetWhitelist, &QAbstractButton::toggled, this, &ThisType::enableApplyButton);
|
||||||
connect(m_ui->checkBypassAuthSubnetWhitelist, &QAbstractButton::toggled, m_ui->IPSubnetWhitelistButton, &QPushButton::setEnabled);
|
connect(m_ui->checkBypassAuthSubnetWhitelist, &QAbstractButton::toggled, m_ui->IPSubnetWhitelistButton, &QPushButton::setEnabled);
|
||||||
|
connect(m_ui->checkClickjacking, &QCheckBox::toggled, this, &ThisType::enableApplyButton);
|
||||||
|
connect(m_ui->checkCSRFProtection, &QCheckBox::toggled, this, &ThisType::enableApplyButton);
|
||||||
connect(m_ui->checkDynDNS, &QGroupBox::toggled, this, &ThisType::enableApplyButton);
|
connect(m_ui->checkDynDNS, &QGroupBox::toggled, this, &ThisType::enableApplyButton);
|
||||||
connect(m_ui->comboDNSService, qComboBoxCurrentIndexChanged, this, &ThisType::enableApplyButton);
|
connect(m_ui->comboDNSService, qComboBoxCurrentIndexChanged, this, &ThisType::enableApplyButton);
|
||||||
connect(m_ui->domainNameTxt, &QLineEdit::textChanged, this, &ThisType::enableApplyButton);
|
connect(m_ui->domainNameTxt, &QLineEdit::textChanged, this, &ThisType::enableApplyButton);
|
||||||
@ -694,6 +696,9 @@ void OptionsDialog::saveOptions()
|
|||||||
pref->setWebUiPassword(webUiPassword());
|
pref->setWebUiPassword(webUiPassword());
|
||||||
pref->setWebUiLocalAuthEnabled(!m_ui->checkBypassLocalAuth->isChecked());
|
pref->setWebUiLocalAuthEnabled(!m_ui->checkBypassLocalAuth->isChecked());
|
||||||
pref->setWebUiAuthSubnetWhitelistEnabled(m_ui->checkBypassAuthSubnetWhitelist->isChecked());
|
pref->setWebUiAuthSubnetWhitelistEnabled(m_ui->checkBypassAuthSubnetWhitelist->isChecked());
|
||||||
|
// Security
|
||||||
|
pref->setWebUiClickjackingProtectionEnabled(m_ui->checkClickjacking->isChecked());
|
||||||
|
pref->setWebUiCSRFProtectionEnabled(m_ui->checkCSRFProtection->isChecked());
|
||||||
// DynDNS
|
// DynDNS
|
||||||
pref->setDynDNSEnabled(m_ui->checkDynDNS->isChecked());
|
pref->setDynDNSEnabled(m_ui->checkDynDNS->isChecked());
|
||||||
pref->setDynDNSService(m_ui->comboDNSService->currentIndex());
|
pref->setDynDNSService(m_ui->comboDNSService->currentIndex());
|
||||||
@ -1096,6 +1101,10 @@ void OptionsDialog::loadOptions()
|
|||||||
m_ui->checkBypassAuthSubnetWhitelist->setChecked(pref->isWebUiAuthSubnetWhitelistEnabled());
|
m_ui->checkBypassAuthSubnetWhitelist->setChecked(pref->isWebUiAuthSubnetWhitelistEnabled());
|
||||||
m_ui->IPSubnetWhitelistButton->setEnabled(m_ui->checkBypassAuthSubnetWhitelist->isChecked());
|
m_ui->IPSubnetWhitelistButton->setEnabled(m_ui->checkBypassAuthSubnetWhitelist->isChecked());
|
||||||
|
|
||||||
|
// Security
|
||||||
|
m_ui->checkClickjacking->setChecked(pref->isWebUiClickjackingProtectionEnabled());
|
||||||
|
m_ui->checkCSRFProtection->setChecked(pref->isWebUiCSRFProtectionEnabled());
|
||||||
|
|
||||||
m_ui->checkDynDNS->setChecked(pref->isDynDNSEnabled());
|
m_ui->checkDynDNS->setChecked(pref->isDynDNSEnabled());
|
||||||
m_ui->comboDNSService->setCurrentIndex(static_cast<int>(pref->getDynDNSService()));
|
m_ui->comboDNSService->setCurrentIndex(static_cast<int>(pref->getDynDNSService()));
|
||||||
m_ui->domainNameTxt->setText(pref->getDynDomainName());
|
m_ui->domainNameTxt->setText(pref->getDynDomainName());
|
||||||
|
@ -3168,6 +3168,20 @@ Use ';' to split multiple entries. Can use wildcard '*'.</string>
|
|||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="checkClickjacking">
|
||||||
|
<property name="text">
|
||||||
|
<string>Enable clickjacking protection</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="checkCSRFProtection">
|
||||||
|
<property name="text">
|
||||||
|
<string>Enable Cross-Site Request Forgery (CSRF) protection</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QGroupBox" name="checkDynDNS">
|
<widget class="QGroupBox" name="checkDynDNS">
|
||||||
<property name="title">
|
<property name="title">
|
||||||
|
@ -205,6 +205,9 @@ void AppController::preferencesAction()
|
|||||||
for (const Utils::Net::Subnet &subnet : copyAsConst(pref->getWebUiAuthSubnetWhitelist()))
|
for (const Utils::Net::Subnet &subnet : copyAsConst(pref->getWebUiAuthSubnetWhitelist()))
|
||||||
authSubnetWhitelistStringList << Utils::Net::subnetToString(subnet);
|
authSubnetWhitelistStringList << Utils::Net::subnetToString(subnet);
|
||||||
data["bypass_auth_subnet_whitelist"] = authSubnetWhitelistStringList.join("\n");
|
data["bypass_auth_subnet_whitelist"] = authSubnetWhitelistStringList.join("\n");
|
||||||
|
// Security
|
||||||
|
data["web_ui_clickjacking_protection_enabled"] = pref->isWebUiClickjackingProtectionEnabled();
|
||||||
|
data["web_ui_csrf_protection_enabled"] = pref->isWebUiCSRFProtectionEnabled();
|
||||||
// Update my dynamic domain name
|
// Update my dynamic domain name
|
||||||
data["dyndns_enabled"] = pref->isDynDNSEnabled();
|
data["dyndns_enabled"] = pref->isDynDNSEnabled();
|
||||||
data["dyndns_service"] = pref->getDynDNSService();
|
data["dyndns_service"] = pref->getDynDNSService();
|
||||||
@ -479,6 +482,11 @@ void AppController::setPreferencesAction()
|
|||||||
// recognize new lines and commas as delimiters
|
// recognize new lines and commas as delimiters
|
||||||
pref->setWebUiAuthSubnetWhitelist(m["bypass_auth_subnet_whitelist"].toString().split(QRegularExpression("\n|,"), QString::SkipEmptyParts));
|
pref->setWebUiAuthSubnetWhitelist(m["bypass_auth_subnet_whitelist"].toString().split(QRegularExpression("\n|,"), QString::SkipEmptyParts));
|
||||||
}
|
}
|
||||||
|
// Security
|
||||||
|
if (m.contains("web_ui_clickjacking_protection_enabled"))
|
||||||
|
pref->setWebUiClickjackingProtectionEnabled(m["web_ui_clickjacking_protection_enabled"].toBool());
|
||||||
|
if (m.contains("web_ui_csrf_protection_enabled"))
|
||||||
|
pref->setWebUiCSRFProtectionEnabled(m["web_ui_csrf_protection_enabled"].toBool());
|
||||||
// Update my dynamic domain name
|
// Update my dynamic domain name
|
||||||
if (m.contains("dyndns_enabled"))
|
if (m.contains("dyndns_enabled"))
|
||||||
pref->setDynDNSEnabled(m["dyndns_enabled"].toBool());
|
pref->setDynDNSEnabled(m["dyndns_enabled"].toBool());
|
||||||
|
@ -428,6 +428,9 @@ void WebApplication::configure()
|
|||||||
m_currentLocale = newLocale;
|
m_currentLocale = newLocale;
|
||||||
m_translatedFiles.clear();
|
m_translatedFiles.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_isClickjackingProtectionEnabled = pref->isWebUiClickjackingProtectionEnabled();
|
||||||
|
m_isCSRFProtectionEnabled = pref->isWebUiCSRFProtectionEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
void WebApplication::registerAPIController(const QString &scope, APIController *controller)
|
void WebApplication::registerAPIController(const QString &scope, APIController *controller)
|
||||||
@ -512,9 +515,11 @@ Http::Response WebApplication::processRequest(const Http::Request &request, cons
|
|||||||
clear();
|
clear();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// block cross-site requests
|
// block suspicious requests
|
||||||
if (isCrossSiteRequest(m_request) || !validateHostHeader(m_domainList))
|
if ((m_isCSRFProtectionEnabled && isCrossSiteRequest(m_request))
|
||||||
|
|| !validateHostHeader(m_domainList)) {
|
||||||
throw UnauthorizedHTTPError();
|
throw UnauthorizedHTTPError();
|
||||||
|
}
|
||||||
|
|
||||||
sessionInitialize();
|
sessionInitialize();
|
||||||
doProcessRequest();
|
doProcessRequest();
|
||||||
@ -525,11 +530,13 @@ Http::Response WebApplication::processRequest(const Http::Request &request, cons
|
|||||||
print(error.message(), Http::CONTENT_TYPE_TXT);
|
print(error.message(), Http::CONTENT_TYPE_TXT);
|
||||||
}
|
}
|
||||||
|
|
||||||
// avoid clickjacking attacks
|
|
||||||
header(Http::HEADER_X_FRAME_OPTIONS, "SAMEORIGIN");
|
|
||||||
header(Http::HEADER_X_XSS_PROTECTION, "1; mode=block");
|
header(Http::HEADER_X_XSS_PROTECTION, "1; mode=block");
|
||||||
header(Http::HEADER_X_CONTENT_TYPE_OPTIONS, "nosniff");
|
header(Http::HEADER_X_CONTENT_TYPE_OPTIONS, "nosniff");
|
||||||
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';");
|
|
||||||
|
if (m_isClickjackingProtectionEnabled) {
|
||||||
|
header(Http::HEADER_X_FRAME_OPTIONS, "SAMEORIGIN");
|
||||||
|
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';");
|
||||||
|
}
|
||||||
|
|
||||||
return response();
|
return response();
|
||||||
}
|
}
|
||||||
|
@ -142,4 +142,8 @@ private:
|
|||||||
};
|
};
|
||||||
QMap<QString, TranslatedFile> m_translatedFiles;
|
QMap<QString, TranslatedFile> m_translatedFiles;
|
||||||
QString m_currentLocale;
|
QString m_currentLocale;
|
||||||
|
|
||||||
|
// security related
|
||||||
|
bool m_isClickjackingProtectionEnabled;
|
||||||
|
bool m_isCSRFProtectionEnabled;
|
||||||
};
|
};
|
||||||
|
@ -437,26 +437,35 @@
|
|||||||
</div>
|
</div>
|
||||||
<div style="padding-left: 10px;"><a target="_blank" href="https://httpd.apache.org/docs/current/ssl/ssl_faq.html#aboutcerts">QBT_TR(Information about certificates)QBT_TR[CONTEXT=HttpServer]</a></div>
|
<div style="padding-left: 10px;"><a target="_blank" href="https://httpd.apache.org/docs/current/ssl/ssl_faq.html#aboutcerts">QBT_TR(Information about certificates)QBT_TR[CONTEXT=HttpServer]</a></div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset class="settings">
|
<fieldset class="settings">
|
||||||
<legend>QBT_TR(Authentication)QBT_TR[CONTEXT=OptionsDialog]</legend>
|
<legend>QBT_TR(Authentication)QBT_TR[CONTEXT=OptionsDialog]</legend>
|
||||||
|
<div class="formRow">
|
||||||
|
<label for="webui_username_text" class="leftLabelSmall">QBT_TR(Username:)QBT_TR[CONTEXT=OptionsDialog]</label><input type="text" id="webui_username_text" />
|
||||||
|
</div>
|
||||||
|
<div class="formRow">
|
||||||
|
<label for="webui_password_text" class="leftLabelSmall">QBT_TR(Password:)QBT_TR[CONTEXT=OptionsDialog]</label><input type="password" id="webui_password_text" />
|
||||||
|
</div>
|
||||||
|
<div class="formRow">
|
||||||
|
<input type="checkbox" id="bypass_local_auth_checkbox" />
|
||||||
|
<label for="bypass_local_auth_checkbox">QBT_TR(Bypass authentication for clients on localhost)QBT_TR[CONTEXT=OptionsDialog]</label>
|
||||||
|
</div>
|
||||||
|
<div class="formRow">
|
||||||
|
<input type="checkbox" id="bypass_auth_subnet_whitelist_checkbox" onclick="updateBypasssAuthSettings();" />
|
||||||
|
<label for="bypass_auth_subnet_whitelist_checkbox">QBT_TR(Bypass authentication for clients in whitelisted IP subnets)QBT_TR[CONTEXT=OptionsDialog]</label>
|
||||||
|
</div>
|
||||||
|
<div class="formRow" style="padding-left: 30px; padding-top: 5px;">
|
||||||
|
<textarea id="bypass_auth_subnet_whitelist_textarea" rows="5" cols="48" placeholder="Example: 172.17.32.0/24, fdff:ffff:c8::/40"></textarea>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
<div class="formRow">
|
<div class="formRow">
|
||||||
<label for="webui_username_text" class="leftLabelSmall">QBT_TR(Username:)QBT_TR[CONTEXT=OptionsDialog]</label><input type="text" id="webui_username_text" />
|
<input type="checkbox" id="clickjacking_protection_checkbox" />
|
||||||
|
<label for="clickjacking_protection_checkbox">QBT_TR(Enable clickjacking protection)QBT_TR[CONTEXT=OptionsDialog]</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="formRow">
|
<div class="formRow">
|
||||||
<label for="webui_password_text" class="leftLabelSmall">QBT_TR(Password:)QBT_TR[CONTEXT=OptionsDialog]</label><input type="password" id="webui_password_text" />
|
<input type="checkbox" id="csrf_protection_checkbox" />
|
||||||
</div>
|
<label for="csrf_protection_checkbox">QBT_TR(Enable Cross-Site Request Forgery (CSRF) protection)QBT_TR[CONTEXT=OptionsDialog]</label>
|
||||||
<div class="formRow">
|
|
||||||
<input type="checkbox" id="bypass_local_auth_checkbox" />
|
|
||||||
<label for="bypass_local_auth_checkbox">QBT_TR(Bypass authentication for clients on localhost)QBT_TR[CONTEXT=OptionsDialog]</label>
|
|
||||||
</div>
|
|
||||||
<div class="formRow">
|
|
||||||
<input type="checkbox" id="bypass_auth_subnet_whitelist_checkbox" onclick="updateBypasssAuthSettings();" />
|
|
||||||
<label for="bypass_auth_subnet_whitelist_checkbox">QBT_TR(Bypass authentication for clients in whitelisted IP subnets)QBT_TR[CONTEXT=OptionsDialog]</label>
|
|
||||||
</div>
|
|
||||||
<div class="formRow" style="padding-left: 30px; padding-top: 5px;">
|
|
||||||
<textarea id="bypass_auth_subnet_whitelist_textarea" rows="5" cols="48" placeholder="Example: 172.17.32.0/24, fdff:ffff:c8::/40"></textarea>
|
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
@ -1022,6 +1031,10 @@
|
|||||||
$('bypass_auth_subnet_whitelist_textarea').setProperty('value', pref.bypass_auth_subnet_whitelist);
|
$('bypass_auth_subnet_whitelist_textarea').setProperty('value', pref.bypass_auth_subnet_whitelist);
|
||||||
updateBypasssAuthSettings();
|
updateBypasssAuthSettings();
|
||||||
|
|
||||||
|
// Security
|
||||||
|
$('clickjacking_protection_checkbox').setProperty('checked', pref.web_ui_clickjacking_protection_enabled);
|
||||||
|
$('csrf_protection_checkbox').setProperty('checked', pref.web_ui_csrf_protection_enabled);
|
||||||
|
|
||||||
// Update my dynamic domain name
|
// Update my dynamic domain name
|
||||||
$('use_dyndns_checkbox').setProperty('checked', pref.dyndns_enabled);
|
$('use_dyndns_checkbox').setProperty('checked', pref.dyndns_enabled);
|
||||||
$('dyndns_select').setProperty('value', pref.dyndns_service);
|
$('dyndns_select').setProperty('value', pref.dyndns_service);
|
||||||
@ -1313,6 +1326,9 @@
|
|||||||
settings.set('bypass_auth_subnet_whitelist_enabled', $('bypass_auth_subnet_whitelist_checkbox').getProperty('checked'));
|
settings.set('bypass_auth_subnet_whitelist_enabled', $('bypass_auth_subnet_whitelist_checkbox').getProperty('checked'));
|
||||||
settings.set('bypass_auth_subnet_whitelist', $('bypass_auth_subnet_whitelist_textarea').getProperty('value'));
|
settings.set('bypass_auth_subnet_whitelist', $('bypass_auth_subnet_whitelist_textarea').getProperty('value'));
|
||||||
|
|
||||||
|
settings.set('web_ui_clickjacking_protection_enabled', $('clickjacking_protection_checkbox').getProperty('checked'));
|
||||||
|
settings.set('web_ui_csrf_protection_enabled', $('csrf_protection_checkbox').getProperty('checked'));
|
||||||
|
|
||||||
// Update my dynamic domain name
|
// Update my dynamic domain name
|
||||||
settings.set('dyndns_enabled', $('use_dyndns_checkbox').getProperty('checked'));
|
settings.set('dyndns_enabled', $('use_dyndns_checkbox').getProperty('checked'));
|
||||||
settings.set('dyndns_service', $('dyndns_select').getProperty('value'));
|
settings.set('dyndns_service', $('dyndns_select').getProperty('value'));
|
||||||
|
Loading…
x
Reference in New Issue
Block a user