compatibility: Add more strict desktop app blocking.

This allows us to block use of the desktop app with insecure versions
(we simply fail to load the Zulip webapp at all, instead rendering an
error page).

For now we block only versions that are known to be both insecure and
not auto-updating, but we can easily adjust these parameters in the
future.
This commit is contained in:
Tim Abbott 2020-03-24 18:00:28 -07:00
parent 6fb438277e
commit d9bb6d0081
7 changed files with 96 additions and 22 deletions

View File

@ -1297,12 +1297,16 @@ input.new-organization-button {
} }
.error_page { .error_page {
padding: 20px 0px;
min-height: calc(100vh - 290px); min-height: calc(100vh - 290px);
height: 100%;
background-color: hsl(163, 42%, 85%); background-color: hsl(163, 42%, 85%);
font-family: 'Source Sans Pro', Helvetica, Arial, sans-serif; font-family: 'Source Sans Pro', Helvetica, Arial, sans-serif;
} }
.error_page .container {
padding: 20px 0px;
}
.error_page .row-fluid { .error_page .row-fluid {
margin-top: 60px; margin-top: 60px;
} }

View File

@ -29,11 +29,12 @@
<div data-process="insecure-desktop-app" class="alert alert-info red"> <div data-process="insecure-desktop-app" class="alert alert-info red">
<span class="close" data-dismiss="alert" aria-label="{{ _('Close') }}">&times;</span> <span class="close" data-dismiss="alert" aria-label="{{ _('Close') }}">&times;</span>
<div data-step="1"> <div data-step="1">
You are using an old, insecure version of the Zulip {% trans %}
desktop app that cannot auto-update. You are using an old version of the Zulip desktop app with known security bugs.
<a class="alert-link" href="https://zulipchat.com/apps" target="_blank"> <a class="alert-link" href="https://zulipchat.com/apps" target="_blank">
Download the latest version. Download the latest version.
</a> </a>
{% endtrans %}
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,40 @@
{% extends "zerver/portico.html" %}
{% block content %}
<div class="error_page">
<div class="container">
<div class="row-fluid">
<img src="/static/images/400art.svg" alt=""/>
<div class="errorbox config-error">
<div class="errorcontent">
<h1 class="lead">{{ _('Update required') }}</h1>
<p>
{% trans %}
You are using old version of the Zulip desktop
app that is no longer supported.
{% endtrans %}
</p>
{% if auto_update_broken %}
<p>
{% trans %}
The auto-update feature in this old version of
Zulip desktop app no longer works.
{% endtrans %}
</p>
{% endif %}
<p>
<a href="https://zulipchat.com/apps" target="_blank">
{{ _("Download the latest release.") }}
</a>
</p>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,5 +1,6 @@
from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_classes import ZulipTestCase
from zerver.views.compatibility import find_mobile_os, version_lt from zerver.views.compatibility import find_mobile_os, version_lt, is_outdated_desktop_app
class VersionTest(ZulipTestCase): class VersionTest(ZulipTestCase):
data = [case.split() for case in ''' data = [case.split() for case in '''
@ -93,13 +94,11 @@ class CompatibilityTest(ZulipTestCase):
assert False # nocoverage assert False # nocoverage
def test_insecure_desktop_app(self) -> None: def test_insecure_desktop_app(self) -> None:
from zerver.views.compatibility import is_outdated_desktop_app self.assertEqual(is_outdated_desktop_app('ZulipDesktop/0.5.2 (Mac)'), (True, True, True))
self.assertEqual(is_outdated_desktop_app('ZulipElectron/2.3.82 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Zulip/2.3.82 Chrome/61.0.3163.100 Electron/2.0.9 Safari/537.36'), (True, True, True))
self.assertEqual(is_outdated_desktop_app('ZulipElectron/4.0.0 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Zulip/4.0.3 Chrome/66.0.3359.181 Electron/3.1.10 Safari/537.36'), (True, False, False))
self.assertEqual(is_outdated_desktop_app('ZulipElectron/4.0.3 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Zulip/4.0.3 Chrome/66.0.3359.181 Electron/3.1.10 Safari/537.36'), (False, False, False))
self.assertTrue(is_outdated_desktop_app('ZulipDesktop/0.5.2 (Mac)')) self.assertEqual(is_outdated_desktop_app('Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36'), (False, False, False))
self.assertTrue(is_outdated_desktop_app('ZulipElectron/2.3.82 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Zulip/2.3.82 Chrome/61.0.3163.100 Electron/2.0.9 Safari/537.36'))
self.assertFalse(is_outdated_desktop_app('ZulipElectron/4.0.0 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Zulip/4.0.3 Chrome/66.0.3359.181 Electron/3.1.10 Safari/537.36'))
self.assertFalse(is_outdated_desktop_app('ZulipElectron/4.0.3 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Zulip/4.0.3 Chrome/66.0.3359.181 Electron/3.1.10 Safari/537.36'))
self.assertFalse(is_outdated_desktop_app('Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36')) self.assertEqual(is_outdated_desktop_app(''), (False, False, False))
self.assertFalse(is_outdated_desktop_app(''))

View File

@ -390,6 +390,15 @@ class HomeTest(ZulipTestCase):
html = result.content.decode('utf-8') html = result.content.decode('utf-8')
self.assertIn('Accept the new Terms of Service', html) self.assertIn('Accept the new Terms of Service', html)
def test_banned_desktop_app_versions(self) -> None:
user = self.example_user('hamlet')
self.login_user(user)
result = self.client_get('/',
HTTP_USER_AGENT="ZulipElectron/2.3.82")
html = result.content.decode('utf-8')
self.assertIn('You are using old version of the Zulip desktop', html)
def test_terms_of_service_first_time_template(self) -> None: def test_terms_of_service_first_time_template(self) -> None:
user = self.example_user('hamlet') user = self.example_user('hamlet')
self.login_user(user) self.login_user(user)

View File

@ -88,16 +88,25 @@ def check_global_compatibility(request: HttpRequest) -> HttpResponse:
return json_error(legacy_compatibility_error_message) return json_error(legacy_compatibility_error_message)
return json_success() return json_success()
def is_outdated_desktop_app(user_agent_str: str) -> bool: def is_outdated_desktop_app(user_agent_str: str) -> Tuple[bool, bool, bool]:
# Returns (insecure, banned, auto_update_broken
user_agent = parse_user_agent(user_agent_str) user_agent = parse_user_agent(user_agent_str)
if user_agent['name'] == 'ZulipDesktop': if user_agent['name'] == 'ZulipDesktop':
# The deprecated QT/webkit based desktop app, last updated in ~2016. # The deprecated QT/webkit based desktop app, last updated in ~2016.
return True return (True, True, True)
if user_agent['name'] == 'ZulipElectron' and version_lt(user_agent['version'], '4.0.0'): if user_agent['name'] != 'ZulipElectron':
# Versions of the modern Electron-based Zulip desktop app with return (False, False, False)
# known security issues. Versions before 2.3.82 won't
# auto-update; we may want a special notice to distinguish if version_lt(user_agent['version'], '4.0.0'):
# those from modern releases. # Version 2.3.82 and older (aka <4.0.0) of the modern
return True # Electron-based Zulip desktop app with known security issues.
return False # won't auto-update; we may want a special notice to
# distinguish those from modern releases.
return (True, True, True)
if version_lt(user_agent['version'], '4.0.3'):
# Other insecure versions should just warn.
return (True, False, False)
return (False, False, False)

View File

@ -146,6 +146,18 @@ def home(request: HttpRequest) -> HttpResponse:
@zulip_login_required @zulip_login_required
def home_real(request: HttpRequest) -> HttpResponse: def home_real(request: HttpRequest) -> HttpResponse:
# Before we do any real work, check if the app is banned.
(insecure_desktop_app, banned_desktop_app, auto_update_broken) = is_outdated_desktop_app(
request.META.get("HTTP_USER_AGENT", ""))
if banned_desktop_app:
return render(
request,
'zerver/insecure_desktop_app.html',
context={
"auto_update_broken": auto_update_broken,
}
)
# We need to modify the session object every two weeks or it will expire. # We need to modify the session object every two weeks or it will expire.
# This line makes reloading the page a sufficient action to keep the # This line makes reloading the page a sufficient action to keep the
# session alive. # session alive.
@ -228,7 +240,7 @@ def home_real(request: HttpRequest) -> HttpResponse:
debug_mode = settings.DEBUG, debug_mode = settings.DEBUG,
test_suite = settings.TEST_SUITE, test_suite = settings.TEST_SUITE,
poll_timeout = settings.POLL_TIMEOUT, poll_timeout = settings.POLL_TIMEOUT,
insecure_desktop_app = is_outdated_desktop_app(request.META.get("HTTP_USER_AGENT", "")), insecure_desktop_app = insecure_desktop_app,
login_page = settings.HOME_NOT_LOGGED_IN, login_page = settings.HOME_NOT_LOGGED_IN,
root_domain_uri = settings.ROOT_DOMAIN_URI, root_domain_uri = settings.ROOT_DOMAIN_URI,
max_file_upload_size = settings.MAX_FILE_UPLOAD_SIZE, max_file_upload_size = settings.MAX_FILE_UPLOAD_SIZE,