From 2524ddaf9485e484c87f2cea51414fc0d362187b Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Mon, 16 Sep 2019 12:10:17 +1000 Subject: Make setting the Content-Security-Policy header optional so it doesn't break website mode shares --- onionshare/settings.py | 1 + onionshare/web/web.py | 24 ++++++++++++-------- onionshare_gui/settings_dialog.py | 25 +++++++++++++++++++++ share/locale/en.json | 1 + tests/GuiWebsiteTest.py | 15 +++++++++++++ ...cal_onionshare_website_mode_csp_enabled_test.py | 26 ++++++++++++++++++++++ tests/local_onionshare_website_mode_test.py | 1 + tests/test_onionshare_settings.py | 3 ++- 8 files changed, 86 insertions(+), 10 deletions(-) create mode 100644 tests/local_onionshare_website_mode_csp_enabled_test.py diff --git a/onionshare/settings.py b/onionshare/settings.py index 762c6dc2..28523b89 100644 --- a/onionshare/settings.py +++ b/onionshare/settings.py @@ -114,6 +114,7 @@ class Settings(object): 'password': '', 'hidservauth_string': '', 'data_dir': self.build_default_data_dir(), + 'csp_header_enabled': True, 'locale': None # this gets defined in fill_in_defaults() } self._settings = {} diff --git a/onionshare/web/web.py b/onionshare/web/web.py index ecd9edc2..825e690c 100644 --- a/onionshare/web/web.py +++ b/onionshare/web/web.py @@ -91,15 +91,6 @@ class Web: # Monkey-patch in the fix from https://github.com/pallets/flask/commit/99c99c4c16b1327288fd76c44bc8635a1de452bc Flask.select_jinja_autoescape = self._safe_select_jinja_autoescape - self.security_headers = [ - ('Content-Security-Policy', 'default-src \'self\'; style-src \'self\'; script-src \'self\'; img-src \'self\' data:;'), - ('X-Frame-Options', 'DENY'), - ('X-Xss-Protection', '1; mode=block'), - ('X-Content-Type-Options', 'nosniff'), - ('Referrer-Policy', 'no-referrer'), - ('Server', 'OnionShare') - ] - self.q = queue.Queue() self.password = None @@ -293,6 +284,20 @@ class Web: pass self.running = False + def set_security_headers(self): + """ + Set the security headers for the web service each time we start it. + """ + self.security_headers = [ + ('X-Frame-Options', 'DENY'), + ('X-Xss-Protection', '1; mode=block'), + ('X-Content-Type-Options', 'nosniff'), + ('Referrer-Policy', 'no-referrer'), + ('Server', 'OnionShare') + ] + if self.common.settings.get('csp_header_enabled'): + self.security_headers.append(('Content-Security-Policy', 'default-src \'self\'; style-src \'self\'; script-src \'self\'; img-src \'self\' data:;')) + def start(self, port, stay_open=False, public_mode=False, password=None): """ Start the flask web server. @@ -315,6 +320,7 @@ class Web: host = '127.0.0.1' self.running = True + self.set_security_headers() self.app.run(host=host, port=port, threaded=True) def stop(self, port): diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py index 25165688..432645e6 100644 --- a/onionshare_gui/settings_dialog.py +++ b/onionshare_gui/settings_dialog.py @@ -214,10 +214,28 @@ class SettingsDialog(QtWidgets.QDialog): self.close_after_first_download_checkbox.setText(strings._("gui_settings_close_after_first_download_option")) individual_downloads_label = QtWidgets.QLabel(strings._("gui_settings_individual_downloads_label")) + # Option to disable Content Security Policy (for website sharing) + self.csp_header_enabled_checkbox = QtWidgets.QCheckBox() + self.csp_header_enabled_checkbox.setCheckState(QtCore.Qt.Checked) + self.csp_header_enabled_checkbox.setText(strings._("gui_settings_csp_header_enabled_option")) + csp_header_label = QtWidgets.QLabel(strings._("gui_settings_whats_this").format("https://github.com/micahflee/onionshare/wiki/Content-Security-Policy")) + csp_header_label.setStyleSheet(self.common.css['settings_whats_this']) + csp_header_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) + csp_header_label.setOpenExternalLinks(True) + csp_header_label.setMinimumSize(csp_header_label.sizeHint()) + csp_header_layout = QtWidgets.QHBoxLayout() + csp_header_layout.addWidget(self.csp_header_enabled_checkbox) + csp_header_layout.addWidget(csp_header_label) + csp_header_layout.addStretch() + csp_header_layout.setContentsMargins(0,0,0,0) + self.csp_header_widget = QtWidgets.QWidget() + self.csp_header_widget.setLayout(csp_header_layout) + # Sharing options layout sharing_group_layout = QtWidgets.QVBoxLayout() sharing_group_layout.addWidget(self.close_after_first_download_checkbox) sharing_group_layout.addWidget(individual_downloads_label) + sharing_group_layout.addWidget(self.csp_header_widget) sharing_group = QtWidgets.QGroupBox(strings._("gui_settings_sharing_label")) sharing_group.setLayout(sharing_group_layout) @@ -517,6 +535,12 @@ class SettingsDialog(QtWidgets.QDialog): else: self.close_after_first_download_checkbox.setCheckState(QtCore.Qt.Unchecked) + csp_header_enabled = self.old_settings.get('csp_header_enabled') + if csp_header_enabled: + self.csp_header_enabled_checkbox.setCheckState(QtCore.Qt.Checked) + else: + self.csp_header_enabled_checkbox.setCheckState(QtCore.Qt.Unchecked) + autostart_timer = self.old_settings.get('autostart_timer') if autostart_timer: self.autostart_timer_checkbox.setCheckState(QtCore.Qt.Checked) @@ -982,6 +1006,7 @@ class SettingsDialog(QtWidgets.QDialog): settings.load() # To get the last update timestamp settings.set('close_after_first_download', self.close_after_first_download_checkbox.isChecked()) + settings.set('csp_header_enabled', self.csp_header_enabled_checkbox.isChecked()) settings.set('autostart_timer', self.autostart_timer_checkbox.isChecked()) settings.set('autostop_timer', self.autostop_timer_checkbox.isChecked()) diff --git a/share/locale/en.json b/share/locale/en.json index aab6153d..6ca5c01f 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -52,6 +52,7 @@ "gui_settings_onion_label": "Onion settings", "gui_settings_sharing_label": "Sharing settings", "gui_settings_close_after_first_download_option": "Stop sharing after files have been sent", + "gui_settings_csp_header_enabled_option": "Enable Content Security Policy header", "gui_settings_individual_downloads_label": "Uncheck to allow downloading individual files", "gui_settings_connection_type_label": "How should OnionShare connect to Tor?", "gui_settings_connection_type_bundled_option": "Use the Tor version built into OnionShare", diff --git a/tests/GuiWebsiteTest.py b/tests/GuiWebsiteTest.py index 7b88bfdf..f58f4aa2 100644 --- a/tests/GuiWebsiteTest.py +++ b/tests/GuiWebsiteTest.py @@ -65,6 +65,20 @@ class GuiWebsiteTest(GuiShareTest): QtTest.QTest.qWait(2000) self.assertTrue('This is a test website hosted by OnionShare' in r.text) + def check_csp_header(self, public_mode, csp_header_enabled): + '''Test that the CSP header is present when enabled or vice versa''' + url = "http://127.0.0.1:{}/".format(self.gui.app.port) + if public_mode: + r = requests.get(url) + else: + r = requests.get(url, auth=requests.auth.HTTPBasicAuth('onionshare', self.gui.website_mode.server_status.web.password)) + + QtTest.QTest.qWait(2000) + if csp_header_enabled: + self.assertTrue('Content-Security-Policy' in r.headers) + else: + self.assertFalse('Content-Security-Policy' in r.headers) + def run_all_website_mode_setup_tests(self): """Tests in website mode prior to starting a share""" self.click_mode(self.gui.website_mode) @@ -92,6 +106,7 @@ class GuiWebsiteTest(GuiShareTest): self.run_all_website_mode_setup_tests() self.run_all_website_mode_started_tests(public_mode, startup_time=2000) self.view_website(public_mode) + self.check_csp_header(public_mode, self.gui.common.settings.get('csp_header_enabled')) self.history_widgets_present(self.gui.website_mode) self.server_is_stopped(self.gui.website_mode, False) self.web_server_is_stopped() diff --git a/tests/local_onionshare_website_mode_csp_enabled_test.py b/tests/local_onionshare_website_mode_csp_enabled_test.py new file mode 100644 index 00000000..3cf79440 --- /dev/null +++ b/tests/local_onionshare_website_mode_csp_enabled_test.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +import pytest +import unittest + +from .GuiWebsiteTest import GuiWebsiteTest + +class LocalWebsiteModeCSPEnabledTest(unittest.TestCase, GuiWebsiteTest): + @classmethod + def setUpClass(cls): + test_settings = { + "csp_header_enabled": True, + } + cls.gui = GuiWebsiteTest.set_up(test_settings) + + @classmethod + def tearDownClass(cls): + GuiWebsiteTest.tear_down() + + @pytest.mark.gui + @pytest.mark.skipif(pytest.__version__ < '2.9', reason="requires newer pytest") + def test_gui(self): + #self.run_all_common_setup_tests() + self.run_all_website_mode_download_tests(False) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/local_onionshare_website_mode_test.py b/tests/local_onionshare_website_mode_test.py index 051adb3c..5a7334a4 100644 --- a/tests/local_onionshare_website_mode_test.py +++ b/tests/local_onionshare_website_mode_test.py @@ -8,6 +8,7 @@ class LocalWebsiteModeTest(unittest.TestCase, GuiWebsiteTest): @classmethod def setUpClass(cls): test_settings = { + "csp_header_enabled": False } cls.gui = GuiWebsiteTest.set_up(test_settings) diff --git a/tests/test_onionshare_settings.py b/tests/test_onionshare_settings.py index 05878899..d46c599b 100644 --- a/tests/test_onionshare_settings.py +++ b/tests/test_onionshare_settings.py @@ -66,7 +66,8 @@ class TestSettings: 'password': '', 'hidservauth_string': '', 'data_dir': os.path.expanduser('~/OnionShare'), - 'public_mode': False + 'public_mode': False, + 'csp_header_enabled': True } for key in settings_obj._settings: # Skip locale, it will not always default to the same thing -- cgit v1.2.3-54-g00ecf