diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e241678b6b..e78de8b57a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -28,7 +28,7 @@ jobs: - ["3.9", "py39"] - ["3.10", "py310"] - ["3.11", "py311"] - - ["3.12.0-alpha.7", "py312"] + - ["3.12.0-rc.1", "py312"] - ["3.9", "docs"] - ["3.9", "coverage"] exclude: @@ -41,7 +41,7 @@ jobs: # macOS/Python 3.11+ is set up for universal2 architecture # which causes build and package selection issues. - { os: ["macos", "macos-11"], config: ["3.11", "py311"] } - - { os: ["macos", "macos-11"], config: ["3.12.0-alpha.7", "py312"] } + - { os: ["macos", "macos-11"], config: ["3.12.0-rc.1", "py312"] } runs-on: ${{ matrix.os[1] }} if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name diff --git a/.meta.toml b/.meta.toml index 37abb88813..f3c3f76d15 100644 --- a/.meta.toml +++ b/.meta.toml @@ -2,7 +2,7 @@ # https://github.com/zopefoundation/meta/tree/master/config/zope-product [meta] template = "zope-product" -commit-id = "dec712c6" +commit-id = "28abbfda" [python] with-pypy = false @@ -46,6 +46,7 @@ testenv-commands = [ coverage-basepython = "python3.8" coverage-command = "coverage run {envdir}/bin/alltests {posargs:-vc}" testenv-additional = [ + "allowlist_externals = *", "", "[testenv:pre-commit]", "basepython = python3", diff --git a/CHANGES.rst b/CHANGES.rst index 98af30d536..0004bde076 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,11 +7,20 @@ since the branch point at Zope 4.1.2. The change log for the previous version, Zope 4, is at https://github.com/zopefoundation/Zope/blob/4.x/CHANGES.rst -5.8.4 (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. + + +5.8.4 (2023-09-06) ------------------ - Disable a ``ZCatalog`` (more precisly: ``Products.PluginIndexes``) - performance test which occasionally fails on GITHUB. + performance test which occasionally fails on GitHub. For details, see `#1136 `_. @@ -20,6 +29,8 @@ https://github.com/zopefoundation/Zope/blob/4.x/CHANGES.rst - Update to newest compatible versions of dependencies. +- Add preliminary support for Python 3.12rc1. + - Make ``mapply`` ``__signature__`` aware. This allows to publish methods decorated via a decorator which sets ``__signature__`` on the wrapper to specify @@ -30,7 +41,13 @@ https://github.com/zopefoundation/Zope/blob/4.x/CHANGES.rst and var keyword parameters. - Make Zope's parameters for denial of service protection configurable - `#1141 _`. + `#1141 `_. + +- Update ``RestrictedPython`` to version 6.2 to mitigate a security problem. + (CVE-2023-41039) + +- Update ``AccessControl`` to version 6.2 to mitigate a security problem. + (CVE-2023-41050) - Added image dimensions to SVG file properties `#1146 `_. diff --git a/buildout.cfg b/buildout.cfg index b347040079..4e6efd881a 100644 --- a/buildout.cfg +++ b/buildout.cfg @@ -16,7 +16,7 @@ parts = requirements sources-dir = develop auto-checkout = - RestrictedPython + [testenv] PYTHONHASHSEED = random diff --git a/constraints.txt b/constraints.txt index 00c8b1df2b..c13d5b7232 100644 --- a/constraints.txt +++ b/constraints.txt @@ -1,9 +1,9 @@ -AccessControl==6.1 +AccessControl==6.2 Acquisition==5.0 AuthEncoding==5.0 BTrees==5.0 -Chameleon==4.0.0 -DateTime==5.1 +Chameleon==4.1.0 +DateTime==5.2 DocumentTemplate==4.4 ExtensionClass==5.0 MultiMapping==5.0 @@ -11,12 +11,12 @@ Paste==3.5.3 PasteDeploy==2.1.1; python_version == '3.7' PasteDeploy==3.0.1; python_version > '3.7' Persistence==4.0.post1 -RestrictedPython==6.1 +RestrictedPython==6.2 WSGIProxy2==0.5.1 WebOb==1.8.7 WebTest==3.0.0 ZConfig==4.0 -ZODB==5.8.0 +ZODB==5.8.1 Zope2==4.0 beautifulsoup4==4.12.2 cffi==1.15.1 @@ -24,14 +24,15 @@ multipart==0.2.4 persistent==5.0 pycparser==2.21 python-gettext==5.0 -pytz==2023.3 +pytz==2023.3.post1 roman==4.1 six==1.16.0 -soupsieve==2.4.1 +soupsieve==2.4.1; python_version == '3.7' +soupsieve==2.5; python_version > '3.7' transaction==3.1.0 waitress==2.1.2 z3c.pt==4.0 -zExceptions==4.3 +zExceptions==5.0 zc.lockfile==3.0.post1 zc.recipe.egg==2.0.7 zodbpickle==3.0.1 @@ -39,7 +40,7 @@ zope.annotation==5.0 zope.browser==3.0 zope.browsermenu==5.0 zope.browserpage==5.0 -zope.browserresource==5.0 +zope.browserresource==5.1 zope.cachedescriptors==5.0 zope.component==6.0 zope.configuration==5.0 @@ -47,32 +48,32 @@ zope.container==5.1 zope.contentprovider==5.0 zope.contenttype==5.0 zope.datetime==5.0.0 -zope.deferredimport==4.4 +zope.deferredimport==5.0 zope.deprecation==5.0 zope.dottedname==6.0 -zope.event==4.6 -zope.exceptions==4.6 +zope.event==5.0 +zope.exceptions==5.0.1 zope.filerepresentation==6.0 zope.globalrequest==2.0 zope.hookable==5.4 -zope.i18n==5.0 +zope.i18n==5.1 zope.i18nmessageid==6.0.1 zope.interface==6.0 -zope.lifecycleevent==4.4 +zope.lifecycleevent==5.0 zope.location==5.0 zope.pagetemplate==5.0 zope.processlifetime==3.0 zope.proxy==5.0.0 zope.ptresource==5.0 -zope.publisher==6.1.0 +zope.publisher==7.0 zope.schema==7.0.1 zope.security==6.1 zope.sequencesort==5.0 -zope.site==4.6.1 -zope.size==4.4 +zope.site==5.0 +zope.size==5.0 zope.structuredtext==5.0 zope.tal==5.0.1 -zope.tales==5.2 +zope.tales==6.0 zope.testbrowser==6.0 zope.testing==5.0.1 zope.traversing==5.0 diff --git a/docs/INSTALL.rst b/docs/INSTALL.rst index e19d9498a5..4a1a8dd679 100644 --- a/docs/INSTALL.rst +++ b/docs/INSTALL.rst @@ -61,6 +61,12 @@ that version number with your desired version. Built-in standard buildout configuration ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. note:: + + The standard buildout configuration is designed to create scripts needed + for developing and testing Zope, it is not for production use. Please use + a custom buildout configuration, a minimal example is shown below. + .. code-block:: console $ wget https://pypi.org/packages/source/Z/Zope/Zope-5.0.tar.gz diff --git a/docs/conf.py b/docs/conf.py index 1470044e91..daf6fe7f07 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -27,7 +27,7 @@ # The short X.Y version version = '5.8' # The full version, including alpha/beta/rc tags -release = '5.8.3' +release = '5.8.4' # -- General configuration --------------------------------------------------- diff --git a/docs/maintenance.rst b/docs/maintenance.rst index 5e5375f7fc..bcff47a0fc 100644 --- a/docs/maintenance.rst +++ b/docs/maintenance.rst @@ -34,7 +34,9 @@ Steps for creating a new Zope release ``auto-checkout`` enter them into ``versions-prod.cfg`` and run ``bin/buildout`` to update ``requirements-full.txt``. -- Garden the change log and check it for spelling issues. +- Garden the change log and check it for spelling issues. Look at the changes + for this new release and decide on the version number. Features should + trigger a feature release. - Check the future PyPI long description for ReST errors:: diff --git a/requirements-full.txt b/requirements-full.txt index 20e466e42b..b07ab70ae4 100644 --- a/requirements-full.txt +++ b/requirements-full.txt @@ -1,10 +1,10 @@ -e git+https://github.com/zopefoundation/Zope.git@master#egg=Zope -AccessControl==6.1 +AccessControl==6.2 Acquisition==5.0 AuthEncoding==5.0 BTrees==5.0 -Chameleon==4.0.0 -DateTime==5.1 +Chameleon==4.1.0 +DateTime==5.2 DocumentTemplate==4.4 ExtensionClass==5.0 MultiMapping==5.0 @@ -12,12 +12,12 @@ Paste==3.5.3 PasteDeploy==2.1.1; python_version == '3.7' PasteDeploy==3.0.1; python_version > '3.7' Persistence==4.0.post1 -RestrictedPython==6.1 +RestrictedPython==6.2 WSGIProxy2==0.5.1 WebOb==1.8.7 WebTest==3.0.0 ZConfig==4.0 -ZODB==5.8.0 +ZODB==5.8.1 Zope2==4.0 beautifulsoup4==4.12.2 cffi==1.15.1 @@ -25,14 +25,15 @@ multipart==0.2.4 persistent==5.0 pycparser==2.21 python-gettext==5.0 -pytz==2023.3 +pytz==2023.3.post1 roman==4.1 six==1.16.0 -soupsieve==2.4.1 +soupsieve==2.4.1; python_version == '3.7' +soupsieve==2.5; python_version > '3.7' transaction==3.1.0 waitress==2.1.2 z3c.pt==4.0 -zExceptions==4.3 +zExceptions==5.0 zc.lockfile==3.0.post1 zc.recipe.egg==2.0.7 zodbpickle==3.0.1 @@ -40,7 +41,7 @@ zope.annotation==5.0 zope.browser==3.0 zope.browsermenu==5.0 zope.browserpage==5.0 -zope.browserresource==5.0 +zope.browserresource==5.1 zope.cachedescriptors==5.0 zope.component==6.0 zope.configuration==5.0 @@ -48,32 +49,32 @@ zope.container==5.1 zope.contentprovider==5.0 zope.contenttype==5.0 zope.datetime==5.0.0 -zope.deferredimport==4.4 +zope.deferredimport==5.0 zope.deprecation==5.0 zope.dottedname==6.0 -zope.event==4.6 -zope.exceptions==4.6 +zope.event==5.0 +zope.exceptions==5.0.1 zope.filerepresentation==6.0 zope.globalrequest==2.0 zope.hookable==5.4 -zope.i18n==5.0 +zope.i18n==5.1 zope.i18nmessageid==6.0.1 zope.interface==6.0 -zope.lifecycleevent==4.4 +zope.lifecycleevent==5.0 zope.location==5.0 zope.pagetemplate==5.0 zope.processlifetime==3.0 zope.proxy==5.0.0 zope.ptresource==5.0 -zope.publisher==6.1.0 +zope.publisher==7.0 zope.schema==7.0.1 zope.security==6.1 zope.sequencesort==5.0 -zope.site==4.6.1 -zope.size==4.4 +zope.site==5.0 +zope.size==5.0 zope.structuredtext==5.0 zope.tal==5.0.1 -zope.tales==5.2 +zope.tales==6.0 zope.testbrowser==6.0 zope.testing==5.0.1 zope.traversing==5.0 diff --git a/setup.py b/setup.py index 8acb9297f0..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.8.4.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..0bc289b3d0 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 @@ -23,6 +24,7 @@ from AccessControl.class_init import InitializeClass from AccessControl.Permission import ApplicationDefaultPermissions from AccessControl.Permissions import view_management_screens +from AccessControl.tainted import TaintedString from Acquisition import aq_base from App import FactoryDispatcher from App.ApplicationManager import ApplicationManager @@ -105,6 +107,49 @@ 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 + + # When came_from contains suspicious code, it will not be a string, + # but an instance of AccessControl.tainted.TaintedString. + # Passing this to urlparse, gives: + # AttributeError: 'str' object has no attribute 'decode' + # This is good, but let's check explicitly. + if isinstance(came_from, TaintedString): + return default + try: + parsed_came_from = urlparse(came_from) + except AttributeError: + return default + parsed_parent_url = urlparse(parent_url) + + # 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 (parsed_parent_url.scheme == parsed_came_from.scheme + and parsed_parent_url.netloc == parsed_came_from.netloc): + return came_from + if (not parsed_came_from.scheme and not parsed_came_from.netloc): + # This is only a path. But some paths can be misinterpreted + # by browsers. + if parsed_came_from.path.startswith("//"): + return default + 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..f7fc6bd8e4 100644 --- a/src/OFS/tests/testApplication.py +++ b/src/OFS/tests/testApplication.py @@ -1,5 +1,6 @@ import unittest +from AccessControl.tainted import TaintedString from Testing.ZopeTestCase import FunctionalTestCase @@ -122,6 +123,67 @@ 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') + + # Tainted local path. came_from can be marked as 'tainted' if it + # suspicious contents. It is not accepted then. + request['came_from'] = TaintedString('/new/path') + self.assertEqual(app.getZMIMainFrameTarget(request), + f'{URL1}/manage_workspace') + + # 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') + + # Anything beginning with '' + ) + self.assertEqual(app.getZMIMainFrameTarget(request), + f'{URL1}/manage_workspace') + + # double slashes as path should not be accepted. + # Try a few forms. + request['came_from'] = '//www.example.org' + self.assertEqual(app.getZMIMainFrameTarget(request), + f'{URL1}/manage_workspace') + request['came_from'] = '////www.example.org' + self.assertEqual(app.getZMIMainFrameTarget(request), + f'{URL1}/manage_workspace') + class ApplicationPublishTests(FunctionalTestCase): diff --git a/src/Zope2/Startup/tests/test_schema.py b/src/Zope2/Startup/tests/test_schema.py index b1b06641f6..cf76478645 100644 --- a/src/Zope2/Startup/tests/test_schema.py +++ b/src/Zope2/Startup/tests/test_schema.py @@ -196,7 +196,7 @@ def test_dos_protection(self): params = ["FORM_%s_LIMIT" % name for name in ("MEMORY", "DISK", "MEMFILE")] - defaults = dict((name, getattr(HTTPRequest, name)) for name in params) + defaults = {name: getattr(HTTPRequest, name) for name in params} try: # missing section diff --git a/tox.ini b/tox.ini index 334a353072..8cc22ce573 100644 --- a/tox.ini +++ b/tox.ini @@ -20,12 +20,16 @@ deps = zc.buildout >= 3.0.1 wheel > 0.37 cffi +setenv = + py312: VIRTUALENV_PIP=23.1.2 + py312: PIP_REQUIRE_VIRTUALENV=0 commands_pre = {envbindir}/buildout -c {toxinidir}/buildout.cfg buildout:directory={envdir} buildout:develop={toxinidir} install alltests commands = # the `layer` argument below suppresses a `ZCatalog` performance test # which occasionally fails on GITHUB {envdir}/bin/alltests --layer '!Products.PluginIndexes' {posargs:-vc} +allowlist_externals = * [testenv:pre-commit] basepython = python3 diff --git a/versions-prod.cfg b/versions-prod.cfg index 0c4c6327d7..ab183a9f6b 100644 --- a/versions-prod.cfg +++ b/versions-prod.cfg @@ -4,38 +4,38 @@ [versions] Zope = Zope2 = 4.0 -AccessControl = 6.1 +AccessControl = 6.2 Acquisition = 5.0 AuthEncoding = 5.0 BTrees = 5.0 -Chameleon = 4.0.0 -DateTime = 5.1 +Chameleon = 4.1.0 +DateTime = 5.2 DocumentTemplate = 4.4 ExtensionClass = 5.0 MultiMapping = 5.0 Paste = 3.5.3 PasteDeploy = 3.0.1 Persistence = 4.0.post1 -RestrictedPython = 6.1 +RestrictedPython = 6.2 WebTest = 3.0.0 WSGIProxy2 = 0.5.1 WebOb = 1.8.7 ZConfig = 4.0 -ZODB = 5.8.0 +ZODB = 5.8.1 beautifulsoup4 = 4.12.2 cffi = 1.15.1 multipart = 0.2.4 persistent = 5.0 pycparser = 2.21 python-gettext = 5.0 -pytz = 2023.3 +pytz = 2023.3.post1 six = 1.16.0 roman = 4.1 -soupsieve = 2.4.1 +soupsieve = 2.5 transaction = 3.1.0 waitress = 2.1.2 z3c.pt = 4.0 -zExceptions = 4.3 +zExceptions = 5.0 zc.lockfile = 3.0.post1 zc.recipe.egg = 2.0.7 zodbpickle = 3.0.1 @@ -43,7 +43,7 @@ zope.annotation = 5.0 zope.browser = 3.0 zope.browsermenu = 5.0 zope.browserpage = 5.0 -zope.browserresource = 5.0 +zope.browserresource = 5.1 zope.cachedescriptors = 5.0 zope.component = 6.0 zope.configuration = 5.0 @@ -51,32 +51,32 @@ zope.container = 5.1 zope.contentprovider = 5.0 zope.contenttype = 5.0 zope.datetime = 5.0.0 -zope.deferredimport = 4.4 +zope.deferredimport = 5.0 zope.deprecation = 5.0 zope.dottedname = 6.0 -zope.event = 4.6 -zope.exceptions = 4.6 +zope.event = 5.0 +zope.exceptions = 5.0.1 zope.filerepresentation = 6.0 zope.globalrequest = 2.0 zope.hookable = 5.4 -zope.i18n = 5.0 +zope.i18n = 5.1 zope.i18nmessageid = 6.0.1 zope.interface = 6.0 -zope.lifecycleevent = 4.4 +zope.lifecycleevent = 5.0 zope.location = 5.0 zope.pagetemplate = 5.0 zope.processlifetime = 3.0 zope.proxy = 5.0.0 zope.ptresource = 5.0 -zope.publisher = 6.1.0 +zope.publisher = 7.0 zope.schema = 7.0.1 zope.security = 6.1 zope.sequencesort = 5.0 -zope.site = 4.6.1 -zope.size = 4.4 +zope.site = 5.0 +zope.size = 5.0 zope.structuredtext = 5.0 zope.tal = 5.0.1 -zope.tales = 5.2 +zope.tales = 6.0 zope.testbrowser = 6.0 zope.testing = 5.0.1 zope.traversing = 5.0 @@ -85,8 +85,10 @@ zope.viewlet = 5.0 [versions:python37] # PasteDeploy 3.x works on Python 3.7 but pulls tons of dependencies PasteDeploy = 2.1.1 +# SoupSieve 2.5 and up requires Python 3.8 +soupsieve = 2.4.1 [versions:python312] # Use the development version here until the final release to test Products # against Python 3.12. -RestrictedPython = 7.0a1.dev0 +RestrictedPython = 7.0a1.dev1 diff --git a/versions.cfg b/versions.cfg index c41956c62b..762c88ba42 100644 --- a/versions.cfg +++ b/versions.cfg @@ -7,38 +7,40 @@ versions = versions # Version pins for development and optional dependencies. Babel = 2.12.1 Jinja2 = 3.1.2 -Missing = 4.2 +Missing = 5.0 MarkupSafe = 2.1.3 Products.BTreeFolder2 = 5.1 Products.ZCatalog = 7.0 -Pygments = 2.15.1 +Pygments = 2.16.1 Record = 4.0 -# sphinx-rtd-theme 1.2.2 requires Sphinx<7 -Sphinx = 6.2.1 +Sphinx = 7.2.5 alabaster = 0.7.13 -certifi = 2023.5.7 -charset-normalizer = 3.1.0 +certifi = 2023.7.22 +charset-normalizer = 3.2.0 collective.recipe.template = 2.2 -# sphinx-rtd-theme 1.2.2 requires docutils<19 +# sphinx-rtd-theme 1.3.0 requires docutils<19 docutils = 0.18.1 five.localsitemanager = 4.0 idna = 3.4 imagesize = 1.4.1 +# XXX importlib-metadata 6.8.0 requires Python 3.8 +importlib-metadata = 6.8.0 mr.developer = 2.0.1 packaging = 23.1 plone.recipe.command = 1.1 requests = 2.31.0 snowballstemmer = 2.2.0 -sphinx-rtd-theme = 1.2.2 -sphinxcontrib-applehelp = 1.0.4 -sphinxcontrib-devhelp = 1.0.2 -sphinxcontrib-htmlhelp = 2.0.1 +sphinx-rtd-theme = 1.3.0 +sphinxcontrib-applehelp = 1.0.7 +sphinxcontrib-devhelp = 1.0.5 +sphinxcontrib-htmlhelp = 2.0.4 sphinxcontrib-jquery = 4.1 sphinxcontrib-jsmath = 1.0.1 -sphinxcontrib-qthelp = 1.0.3 -sphinxcontrib-serializinghtml = 1.1.5 +sphinxcontrib-qthelp = 1.0.6 +sphinxcontrib-serializinghtml = 1.1.9 tempstorage = 6.0 -urllib3 = 2.0.3 +urllib3 = 2.0.4 z3c.checkversions = 2.1 zc.recipe.testrunner = 3.0 -zope.testrunner = 6.0 +zipp = 3.16.2 +zope.testrunner = 6.1