CVE-2021-32674 (Python, published 2021-06-08T18:15Z)
Description: value: Zope is an open-source web application server. This advisory extends the previous advisory at https://github.com/zopefoundation/Zope/security/advisories/GHSA-5pr9-v234-jw36 with additional cases of TAL expression traversal vulnerabilities. Most Python modules are not available for using in TAL expressions that you can add through-the-web, for example in Zope Page Templates. This restriction avoids file system access, for example via the 'os' module. But some of the untrusted modules are available indirectly through Python modules that are available for direct use. By default, you need to have the Manager role to add or edit Zope Page Templates through the web. Only sites that allow untrusted users to add/edit Zope Page Templates through the web are at risk. The problem has been fixed in Zope 5.2.1 and 4.6.1. The workaround is the same as for https://github.com/zopefoundation/Zope/security/advisories/GHSA-5pr9-v234-jw36: A site administrator can restrict adding/editing Zope Page Templates through the web using the standard Zope user/role permission mechanisms. Untrusted users should not be assigned the Zope Manager role and adding/editing Zope Page Templates through the web should be restricted to trusted users only.
Severity: MEDIUM (CVSS v3 base score: 8.8)
Weakness: CWE-22 \u2014 Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
Repository: https://github.com/zopefoundation/Zope
Files changed:
CHANGES.rst
src/OFS/zpt/main.zpt
src/Products/PageTemplates/Expressions.py
src/Products/PageTemplates/expression.py
src/Products/PageTemplates/tests/testExpressions.py
src/Products/PageTemplates/tests/testHTMLTests.py
Commit message: Merge pull request from GHSA-rpcg-f9q6-2mq6

* - Prevent traversal through authorized Python modules in TAL expressions

* - try a more generic solution that doesn't use special casing

* - add suggestions from Maurits

* - integrate remaining suggestions from Maurits

Vulnerable code:
- Update to newest compatible versions of dependencies.
                    sm modules/AccessControl/SecurityManagement/getSecurityManager;
        if name == '_':
            warnings.warn('Traversing to the name `_` is deprecated '
                          'and will be removed in Zope 6.',
                          DeprecationWarning)
        elif name.startswith('_'):
            raise NotFound(name)

            object = traversePathElement(object, name, path_items,
                                         request=request)
            if name == '_':
                warnings.warn('Traversing to the name `_` is deprecated '
                              'and will be removed in Zope 6.',
                              DeprecationWarning)
            elif name.startswith('_'):
                raise NotFound(name)

                base = traversePathElement(base, name, path_items,
                                           request=request)
        with self.assertRaises(NotFound):
        with self.assertRaises(NotFound):
        with self.assertRaises(NotFound):

Fixed code:
- Prevent unauthorized traversal through authorized Python modules in
  TAL expressions
- Update to newest compatible versions of dependencies.

                    sm modules/AccessControl/getSecurityManager;
from AccessControl.SecurityManagement import getSecurityManager
    validate = getSecurityManager().validate
            found = traversePathElement(object, name, path_items,
                                        request=request)

            # Special backwards compatibility exception for the name ``_``,
            # which was often used for translation message factories.
            # Allow and continue traversal.
            if name == '_':
                warnings.warn('Traversing to the name `_` is deprecated '
                              'and will be removed in Zope 6.',
                              DeprecationWarning)
                object = found
                continue

            # All other names starting with ``_`` are disallowed.
            # This emulates what restrictedTraverse does.
            if name.startswith('_'):
                raise NotFound(name)

            # traversePathElement doesn't apply any Zope security policy,
            # so we validate access explicitly here.
            try:
                validate(object, object, name, found)
                object = found
            except Unauthorized:
                # Convert Unauthorized to prevent information disclosures
                raise NotFound(name)

from AccessControl.SecurityManagement import getSecurityManager
        validate = getSecurityManager().validate
                found = traversePathElement(base, name, path_items,
                                            request=request)

                # If traverse_method is something other than
                # ``restrictedTraverse`` then traversal is assumed to be
                # unrestricted. This emulates ``unrestrictedTraverse``
                if cls.traverse_method != 'restrictedTraverse':
                    base = found
                    continue

                # Special backwards compatibility exception for the name ``_``,
                # which was often used for translation message factories.
                # Allow and continue traversal.
                if name == '_':
                    warnings.warn('Traversing to the name `_` is deprecated '
                                  'and will be removed in Zope 6.',
                                  DeprecationWarning)
                    base = found
                    continue

                # All other names starting with ``_`` are disallowed.
                # This emulates what restrictedTraverse does.
                if name.startswith('_'):
                    raise NotFound(name)

                # traversePathElement doesn't apply any Zope security policy,
                # so we validate access explicitly here.
                try:
                    validate(base, base, name, found)
                    base = found
                except Unauthorized:
                    # Convert Unauthorized to prevent information disclosures
                    raise NotFound(name)
from zope.location.interfaces import LocationError
        with self.assertRaises((NotFound, LocationError)):
        with self.assertRaises((NotFound, LocationError)):
from Products.PageTemplates.ZopePageTemplate import ZopePageTemplate
from zope.location.interfaces import LocationError
class AqZopePageTemplate(Implicit, ZopePageTemplate):
    pass


        f.z = AqZopePageTemplate('testing')
        with self.assertRaises((NotFound, LocationError)):
        with self.assertRaises((NotFound, LocationError)):
            t()

    def test_module_traversal(self):
        t = self.folder.z

        # Need to reset to the standard security policy so AccessControl
        # checks are actually performed. The test setup initializes
        # a policy that circumvents those checks.
        SecurityManager.setSecurityPolicy(self.oldPolicy)
        noSecurityManager()

        # The getSecurityManager function is explicitly allowed
        content = ('<p tal:define="a nocall:%s"'
                   '   tal:content="python: a().getUser().getUserName()"/>')
        t.write(content % 'modules/AccessControl/getSecurityManager')
        self.assertEqual(t(), '<p>Anonymous User</p>')

        # Anything else should be unreachable and raise NotFound:
        # Direct access through AccessControl
        t.write('<p tal:define="a nocall:modules/AccessControl/users"/>')
        with self.assertRaises(NotFound):
            t()

        # Indirect access through an intermediary variable
        content = ('<p tal:define="mod nocall:modules/AccessControl;'
                   '               must_fail nocall:mod/users"/>')
        t.write(content)
        with self.assertRaises(NotFound):
            t()

        # Indirect access through an intermediary variable and a dictionary
        content = ('<p tal:define="mod nocall:modules/AccessControl;'
                   '               a_dict python: {\'unsafe\': mod};'
                   '               must_fail nocall: a_dict/unsafe/users"/>')
        t.write(content)