diff --git a/CHANGES.rst b/CHANGES.rst index 1a0fc2cf19..1136f3d2e8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,8 +8,10 @@ The change log for the previous version, Zope 4, is at https://github.com/zopefoundation/Zope/blob/4.x/CHANGES.rst -5.9 (unreleased) ----------------- +5.8.5 (unreleased) +------------------ + +- Tighten down the ZMI frame source logic to only allow site-local sources. - Update to newest compatible versions of dependencies. diff --git a/setup.py b/setup.py index c045010bb5..d80b72e718 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ def _read_file(filename): README = _read_file('README.rst') CHANGES = _read_file('CHANGES.rst') -version = '5.9.dev0' +version = '5.8.5.dev0' setup( name='Zope', diff --git a/src/App/dtml/manage.dtml b/src/App/dtml/manage.dtml index a342e71226..89800b76bc 100644 --- a/src/App/dtml/manage.dtml +++ b/src/App/dtml/manage.dtml @@ -19,7 +19,7 @@ name="manage_menu" marginwidth="2" marginheight="2" /> - " + " name="manage_main" marginwidth="2" marginheight="2" /> diff --git a/src/OFS/Application.py b/src/OFS/Application.py index 1a4ec252d0..2b3212d3fe 100644 --- a/src/OFS/Application.py +++ b/src/OFS/Application.py @@ -16,6 +16,7 @@ import os import sys from logging import getLogger +from urllib.parse import urlparse import Products import transaction @@ -105,6 +106,34 @@ def Redirect(self, destination, URL1): ZopeRedirect = Redirect + @security.protected(view_management_screens) + def getZMIMainFrameTarget(self, REQUEST): + """Utility method to get the right hand side ZMI frame source URL + + For cases where JavaScript is disabled the ZMI uses a simple REQUEST + variable ``came_from`` to set the source URL for the right hand side + ZMI frame. Since this value can be manipulated by the user it must be + sanity-checked first. + """ + parent_url = REQUEST['URL1'] + default = f'{parent_url}/manage_workspace' + came_from = REQUEST.get('came_from', None) + + if not came_from: + return default + + parsed_parent_url = urlparse(parent_url) + parsed_came_from = urlparse(came_from) + + # Only allow a passed-in ``came_from`` URL if it is local (just a path) + # or if the URL scheme and hostname are the same as our own + if (not parsed_came_from.scheme and not parsed_came_from.netloc) or \ + (parsed_parent_url.scheme == parsed_came_from.scheme + and parsed_parent_url.netloc == parsed_came_from.netloc): + return came_from + + return default + def __bobo_traverse__(self, REQUEST, name=None): if name is None: # Make this more explicit, otherwise getattr(self, name) diff --git a/src/OFS/tests/testApplication.py b/src/OFS/tests/testApplication.py index cc23fe6c5f..462e89c3b7 100644 --- a/src/OFS/tests/testApplication.py +++ b/src/OFS/tests/testApplication.py @@ -122,6 +122,44 @@ def test_ZopeVersion(self): self.assertEqual(app.ZopeVersion(major=True), int(pkg_version.split('.')[0])) + def test_getZMIMainFrameTarget(self): + app = self._makeOne() + + for URL1 in ('http://nohost', 'https://nohost/some/path'): + request = {'URL1': URL1} + + # No came_from at all + self.assertEqual(app.getZMIMainFrameTarget(request), + f'{URL1}/manage_workspace') + + # Empty came_from + request['came_from'] = '' + self.assertEqual(app.getZMIMainFrameTarget(request), + f'{URL1}/manage_workspace') + request['came_from'] = None + self.assertEqual(app.getZMIMainFrameTarget(request), + f'{URL1}/manage_workspace') + + # Local (path only) came_from + request['came_from'] = '/new/path' + self.assertEqual(app.getZMIMainFrameTarget(request), + '/new/path') + + # came_from URL outside our own server + request['came_from'] = 'https://www.zope.dev/index.html' + self.assertEqual(app.getZMIMainFrameTarget(request), + f'{URL1}/manage_workspace') + + # came_from with wrong scheme + request['came_from'] = URL1.replace('http', 'ftp') + self.assertEqual(app.getZMIMainFrameTarget(request), + f'{URL1}/manage_workspace') + + # acceptable came_from + request['came_from'] = f'{URL1}/added/path' + self.assertEqual(app.getZMIMainFrameTarget(request), + f'{URL1}/added/path') + class ApplicationPublishTests(FunctionalTestCase):