diff --git a/src/base/preferences.cpp b/src/base/preferences.cpp index 076a73773..ae044fdf2 100644 --- a/src/base/preferences.cpp +++ b/src/base/preferences.cpp @@ -576,6 +576,26 @@ void Preferences::setWebUiPassword(const QString &new_password) 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 { return value("Preferences/WebUI/HTTPS/Enabled", false).toBool(); diff --git a/src/base/preferences.h b/src/base/preferences.h index c9a0188d2..a3b5a2ae7 100644 --- a/src/base/preferences.h +++ b/src/base/preferences.h @@ -194,6 +194,12 @@ public: QString getWebUiPassword() const; void setWebUiPassword(const QString &new_password); + // WebUI security + bool isWebUiClickjackingProtectionEnabled() const; + void setWebUiClickjackingProtectionEnabled(bool enabled); + bool isWebUiCSRFProtectionEnabled() const; + void setWebUiCSRFProtectionEnabled(bool enabled); + // HTTPS bool isWebUiHttpsEnabled() const; void setWebUiHttpsEnabled(bool enabled); diff --git a/src/gui/optionsdlg.cpp b/src/gui/optionsdlg.cpp index dac450aaf..d0ac39d87 100644 --- a/src/gui/optionsdlg.cpp +++ b/src/gui/optionsdlg.cpp @@ -378,6 +378,8 @@ OptionsDialog::OptionsDialog(QWidget *parent) connect(m_ui->checkBypassLocalAuth, &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->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->comboDNSService, qComboBoxCurrentIndexChanged, this, &ThisType::enableApplyButton); connect(m_ui->domainNameTxt, &QLineEdit::textChanged, this, &ThisType::enableApplyButton); @@ -694,6 +696,9 @@ void OptionsDialog::saveOptions() pref->setWebUiPassword(webUiPassword()); pref->setWebUiLocalAuthEnabled(!m_ui->checkBypassLocalAuth->isChecked()); pref->setWebUiAuthSubnetWhitelistEnabled(m_ui->checkBypassAuthSubnetWhitelist->isChecked()); + // Security + pref->setWebUiClickjackingProtectionEnabled(m_ui->checkClickjacking->isChecked()); + pref->setWebUiCSRFProtectionEnabled(m_ui->checkCSRFProtection->isChecked()); // DynDNS pref->setDynDNSEnabled(m_ui->checkDynDNS->isChecked()); pref->setDynDNSService(m_ui->comboDNSService->currentIndex()); @@ -1096,6 +1101,10 @@ void OptionsDialog::loadOptions() m_ui->checkBypassAuthSubnetWhitelist->setChecked(pref->isWebUiAuthSubnetWhitelistEnabled()); 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->comboDNSService->setCurrentIndex(static_cast(pref->getDynDNSService())); m_ui->domainNameTxt->setText(pref->getDynDomainName()); diff --git a/src/gui/optionsdlg.ui b/src/gui/optionsdlg.ui index 0a9afb932..0393990f8 100644 --- a/src/gui/optionsdlg.ui +++ b/src/gui/optionsdlg.ui @@ -3168,6 +3168,20 @@ Use ';' to split multiple entries. Can use wildcard '*'. + + + + Enable clickjacking protection + + + + + + + Enable Cross-Site Request Forgery (CSRF) protection + + + diff --git a/src/webui/api/appcontroller.cpp b/src/webui/api/appcontroller.cpp index 26775b203..fad7bc23e 100644 --- a/src/webui/api/appcontroller.cpp +++ b/src/webui/api/appcontroller.cpp @@ -205,6 +205,9 @@ void AppController::preferencesAction() for (const Utils::Net::Subnet &subnet : copyAsConst(pref->getWebUiAuthSubnetWhitelist())) authSubnetWhitelistStringList << Utils::Net::subnetToString(subnet); 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 data["dyndns_enabled"] = pref->isDynDNSEnabled(); data["dyndns_service"] = pref->getDynDNSService(); @@ -479,6 +482,11 @@ void AppController::setPreferencesAction() // recognize new lines and commas as delimiters 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 if (m.contains("dyndns_enabled")) pref->setDynDNSEnabled(m["dyndns_enabled"].toBool()); diff --git a/src/webui/webapplication.cpp b/src/webui/webapplication.cpp index 401f7de04..bde61c83f 100644 --- a/src/webui/webapplication.cpp +++ b/src/webui/webapplication.cpp @@ -428,6 +428,9 @@ void WebApplication::configure() m_currentLocale = newLocale; m_translatedFiles.clear(); } + + m_isClickjackingProtectionEnabled = pref->isWebUiClickjackingProtectionEnabled(); + m_isCSRFProtectionEnabled = pref->isWebUiCSRFProtectionEnabled(); } void WebApplication::registerAPIController(const QString &scope, APIController *controller) @@ -512,9 +515,11 @@ Http::Response WebApplication::processRequest(const Http::Request &request, cons clear(); try { - // block cross-site requests - if (isCrossSiteRequest(m_request) || !validateHostHeader(m_domainList)) + // block suspicious requests + if ((m_isCSRFProtectionEnabled && isCrossSiteRequest(m_request)) + || !validateHostHeader(m_domainList)) { throw UnauthorizedHTTPError(); + } sessionInitialize(); doProcessRequest(); @@ -525,11 +530,13 @@ Http::Response WebApplication::processRequest(const Http::Request &request, cons 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_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(); } diff --git a/src/webui/webapplication.h b/src/webui/webapplication.h index d43de0f71..eabb08cd1 100644 --- a/src/webui/webapplication.h +++ b/src/webui/webapplication.h @@ -142,4 +142,8 @@ private: }; QMap m_translatedFiles; QString m_currentLocale; + + // security related + bool m_isClickjackingProtectionEnabled; + bool m_isCSRFProtectionEnabled; }; diff --git a/src/webui/www/private/preferences_content.html b/src/webui/www/private/preferences_content.html index 85af4571b..7cf03a480 100644 --- a/src/webui/www/private/preferences_content.html +++ b/src/webui/www/private/preferences_content.html @@ -437,26 +437,35 @@ - -
- QBT_TR(Authentication)QBT_TR[CONTEXT=OptionsDialog] -
- -
-
- -
+
+ QBT_TR(Authentication)QBT_TR[CONTEXT=OptionsDialog] +
+ +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+
- - + +
- - -
-
- + +
@@ -1022,6 +1031,10 @@ $('bypass_auth_subnet_whitelist_textarea').setProperty('value', pref.bypass_auth_subnet_whitelist); 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 $('use_dyndns_checkbox').setProperty('checked', pref.dyndns_enabled); $('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', $('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 settings.set('dyndns_enabled', $('use_dyndns_checkbox').getProperty('checked')); settings.set('dyndns_service', $('dyndns_select').getProperty('value'));