Add an error 401 handler, and make it start counting invalid password guesses instead of 404 errors for rate limiting

This commit is contained in:
Micah Lee 2019-05-20 19:04:50 -07:00
parent 2a50bbc3bc
commit 79b87c3e30
No known key found for this signature in database
GPG Key ID: 403C2657CD994F73
7 changed files with 56 additions and 28 deletions

View File

@ -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("/<slug_candidate>/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.

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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.",

19
share/templates/401.html Normal file
View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<title>OnionShare: 401 Unauthorized Access</title>
<link href="/static/img/favicon.ico" rel="icon" type="image/x-icon" />
<link rel="stylesheet" rel="subresource" type="text/css" href="/static/css/style.css" media="all">
</head>
<body>
<div class="info-wrapper">
<div class="info">
<p><img class="logo" src="/static/img/logo_large.png" title="OnionShare"></p>
<p class="info-header">401 Unauthorized Access</p>
</div>
</div>
</body>
</html>