From 79b87c3e30480708af6d824a19430d24d2693dd4 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Mon, 20 May 2019 19:04:50 -0700 Subject: [PATCH] Add an error 401 handler, and make it start counting invalid password guesses instead of 404 errors for rate limiting --- onionshare/web/web.py | 51 +++++++++++--------- onionshare_gui/mode/receive_mode/__init__.py | 2 +- onionshare_gui/mode/share_mode/__init__.py | 2 +- onionshare_gui/mode/website_mode/__init__.py | 2 +- onionshare_gui/onionshare_gui.py | 5 +- share/locale/en.json | 3 +- share/templates/401.html | 19 ++++++++ 7 files changed, 56 insertions(+), 28 deletions(-) create mode 100644 share/templates/401.html diff --git a/onionshare/web/web.py b/onionshare/web/web.py index 83c441d7..14e2f9b3 100644 --- a/onionshare/web/web.py +++ b/onionshare/web/web.py @@ -44,6 +44,7 @@ class Web(object): REQUEST_UPLOAD_FINISHED = 8 REQUEST_UPLOAD_CANCELED = 9 REQUEST_ERROR_DATA_DIR_CANNOT_CREATE = 10 + REQUEST_INVALID_SLUG = 11 def __init__(self, common, is_gui, mode='share'): self.common = common @@ -55,6 +56,7 @@ class Web(object): template_folder=self.common.get_resource_path('templates')) self.app.secret_key = self.common.random_string(8) self.auth = HTTPBasicAuth() + self.auth.error_handler(self.error401) # Verbose mode? if self.common.verbose: @@ -95,7 +97,8 @@ class Web(object): self.q = queue.Queue() self.slug = None - self.error404_count = 0 + + self.reset_invalid_slugs() self.done = False @@ -141,10 +144,7 @@ class Web(object): return _check_login() @self.app.errorhandler(404) - def page_not_found(e): - """ - 404 error page. - """ + def not_found(e): return self.error404() @self.app.route("//shutdown") @@ -164,18 +164,26 @@ class Web(object): r = make_response(render_template('receive_noscript_xss.html')) return self.add_security_headers(r) + def error401(self): + auth = request.authorization + if auth: + if auth['username'] == 'onionshare' and auth['password'] not in self.invalid_slugs: + print('Invalid password guess: {}'.format(auth['password'])) + self.add_request(Web.REQUEST_INVALID_SLUG, data=auth['password']) + + self.invalid_slugs.append(auth['password']) + self.invalid_slugs_count += 1 + + if self.invalid_slugs_count == 20: + self.add_request(Web.REQUEST_RATE_LIMIT) + self.force_shutdown() + print("Someone has made too many wrong attempts to guess your password, so OnionShare has stopped the server. Start sharing again and send the recipient a new address to share.") + + r = make_response(render_template('401.html'), 401) + return self.add_security_headers(r) + def error404(self): self.add_request(Web.REQUEST_OTHER, request.path) - if request.path != '/favicon.ico': - self.error404_count += 1 - - # In receive mode, with public mode enabled, skip rate limiting 404s - if not self.common.settings.get('public_mode'): - if self.error404_count == 20: - self.add_request(Web.REQUEST_RATE_LIMIT, request.path) - self.force_shutdown() - print("Someone has made too many wrong attempts on your address, which means they could be trying to guess it, so OnionShare has stopped the server. Start sharing again and send the recipient a new address to share.") - r = make_response(render_template('404.html'), 404) return self.add_security_headers(r) @@ -198,7 +206,7 @@ class Web(object): return True return filename.endswith(('.html', '.htm', '.xml', '.xhtml')) - def add_request(self, request_type, path, data=None): + def add_request(self, request_type, path=None, data=None): """ Add a request to the queue, to communicate with the GUI. """ @@ -226,18 +234,15 @@ class Web(object): log_handler.setLevel(logging.WARNING) self.app.logger.addHandler(log_handler) - def check_slug_candidate(self, slug_candidate): - self.common.log('Web', 'check_slug_candidate: slug_candidate={}'.format(slug_candidate)) - if self.common.settings.get('public_mode'): - abort(404) - if not hmac.compare_digest(self.slug, slug_candidate): - abort(404) - def check_shutdown_slug_candidate(self, slug_candidate): self.common.log('Web', 'check_shutdown_slug_candidate: slug_candidate={}'.format(slug_candidate)) if not hmac.compare_digest(self.shutdown_slug, slug_candidate): abort(404) + def reset_invalid_slugs(self): + self.invalid_slugs_count = 0 + self.invalid_slugs = [] + def force_shutdown(self): """ Stop the flask web server, from the context of the flask app. diff --git a/onionshare_gui/mode/receive_mode/__init__.py b/onionshare_gui/mode/receive_mode/__init__.py index 4c0b49ba..d6b1c0f3 100644 --- a/onionshare_gui/mode/receive_mode/__init__.py +++ b/onionshare_gui/mode/receive_mode/__init__.py @@ -113,7 +113,7 @@ class ReceiveMode(Mode): """ # Reset web counters self.web.receive_mode.upload_count = 0 - self.web.error404_count = 0 + self.web.reset_invalid_slugs() # Hide and reset the uploads if we have previously shared self.reset_info_counters() diff --git a/onionshare_gui/mode/share_mode/__init__.py b/onionshare_gui/mode/share_mode/__init__.py index 1ee40ca3..f51fd0bb 100644 --- a/onionshare_gui/mode/share_mode/__init__.py +++ b/onionshare_gui/mode/share_mode/__init__.py @@ -147,7 +147,7 @@ class ShareMode(Mode): """ # Reset web counters self.web.share_mode.download_count = 0 - self.web.error404_count = 0 + self.web.reset_invalid_slugs() # Hide and reset the downloads if we have previously shared self.reset_info_counters() diff --git a/onionshare_gui/mode/website_mode/__init__.py b/onionshare_gui/mode/website_mode/__init__.py index 9018f5cb..c6009ebe 100644 --- a/onionshare_gui/mode/website_mode/__init__.py +++ b/onionshare_gui/mode/website_mode/__init__.py @@ -143,7 +143,7 @@ class WebsiteMode(Mode): """ # Reset web counters self.web.website_mode.visit_count = 0 - self.web.error404_count = 0 + self.web.reset_invalid_slugs() # Hide and reset the downloads if we have previously shared self.reset_info_counters() diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index 9fdf9395..4945ca7e 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -472,7 +472,10 @@ class OnionShareGui(QtWidgets.QMainWindow): if event["type"] == Web.REQUEST_OTHER: if event["path"] != '/favicon.ico' and event["path"] != "/{}/shutdown".format(mode.web.shutdown_slug): - self.status_bar.showMessage('[#{0:d}] {1:s}: {2:s}'.format(mode.web.error404_count, strings._('other_page_loaded'), event["path"])) + self.status_bar.showMessage('{0:s}: {1:s}'.format(strings._('other_page_loaded'), event["path"])) + + if event["type"] == Web.REQUEST_INVALID_SLUG: + self.status_bar.showMessage('[#{0:d}] {1:s}: {2:s}'.format(mode.web.invalid_slugs_count, strings._('invalid_slug_guess'), event["data"])) mode.timer_callback() diff --git a/share/locale/en.json b/share/locale/en.json index 7183e734..6dea9860 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -3,6 +3,7 @@ "not_a_readable_file": "{0:s} is not a readable file.", "no_available_port": "Could not find an available port to start the onion service", "other_page_loaded": "Address loaded", + "invalid_slug_guess": "Invalid password guess", "close_on_autostop_timer": "Stopped because auto-stop timer ran out", "closing_automatically": "Stopped because transfer is complete", "large_filesize": "Warning: Sending a large share could take hours", @@ -34,7 +35,7 @@ "gui_receive_quit_warning": "You're in the process of receiving files. Are you sure you want to quit OnionShare?", "gui_quit_warning_quit": "Quit", "gui_quit_warning_dont_quit": "Cancel", - "error_rate_limit": "Someone has made too many wrong attempts on your address, which means they could be trying to guess it, so OnionShare has stopped the server. Start sharing again and send the recipient a new address to share.", + "error_rate_limit": "Someone has made too many wrong attempts to guess your password, so OnionShare has stopped the server. Start sharing again and send the recipient a new address to share.", "zip_progress_bar_format": "Compressing: %p%", "error_stealth_not_supported": "To use client authorization, you need at least both Tor 0.2.9.1-alpha (or Tor Browser 6.5) and python3-stem 1.5.0.", "error_ephemeral_not_supported": "OnionShare requires at least both Tor 0.2.7.1 and python3-stem 1.4.0.", diff --git a/share/templates/401.html b/share/templates/401.html new file mode 100644 index 00000000..9d3989a3 --- /dev/null +++ b/share/templates/401.html @@ -0,0 +1,19 @@ + + + + + OnionShare: 401 Unauthorized Access + + + + + +
+
+

+

401 Unauthorized Access

+
+
+ + +