VYPR
High severityNVD Advisory· Published May 21, 2021· Updated Aug 3, 2024

Remote Code Execution via traversal in TAL expressions

CVE-2021-32633

Description

Zope is an open-source web application server. In Zope versions prior to 4.6 and 5.2, users can access untrusted modules indirectly through Python modules that are available for direct use. By default, only users with the Manager role can add or edit Zope Page Templates through the web, but sites that allow untrusted users to add/edit Zope Page Templates through the web are at risk from this vulnerability. The problem has been fixed in Zope 5.2 and 4.6. As a workaround, 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.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
ZopePyPI
< 4.64.6
ZopePyPI
>= 5.0, < 5.25.2

Affected products

1

Patches

1
1f8456bf1f90

Merge pull request from GHSA-5pr9-v234-jw36

https://github.com/zopefoundation/ZopeJens VagelpohlMay 21, 2021via ghsa
8 files changed · +84 2
  • CHANGES.rst+3 0 modified
    @@ -11,6 +11,9 @@ https://github.com/zopefoundation/Zope/blob/4.x/CHANGES.rst
     5.2 (unreleased)
     ----------------
     
    +- Prevent traversal to names starting with ``_`` in TAL expressions
    +  and fix path expressions for the ``chameleon.tales`` expression engine.
    +
     - Provide friendlier ZMI error message for the Transaction Undo form
       (`#964 <https://github.com/zopefoundation/Zope/issues/964>`_)
     
    
  • src/Products/PageTemplates/expression.py+10 1 modified
    @@ -1,5 +1,6 @@
     """``chameleon.tales`` expressions."""
     
    +import warnings
     from ast import NodeTransformer
     from ast import parse
     
    @@ -61,8 +62,16 @@ def traverse(cls, base, request, path_items):
     
             while path_items:
                 name = path_items.pop()
    +
    +            if name == '_':
    +                warnings.warn('Traversing to the name `_` is deprecated '
    +                              'and will be removed in Zope 6.',
    +                              DeprecationWarning)
    +            elif name.startswith('_'):
    +                raise NotFound(name)
    +
                 if ITraversable.providedBy(base):
    -                base = getattr(base, cls.traverseMethod)(name)
    +                base = getattr(base, cls.traverse_method)(name)
                 else:
                     base = traversePathElement(base, name, path_items,
                                                request=request)
    
  • src/Products/PageTemplates/Expressions.py+9 0 modified
    @@ -17,6 +17,7 @@
     """
     
     import logging
    +import warnings
     
     import OFS.interfaces
     from AccessControl import safe_builtins
    @@ -74,6 +75,14 @@ def boboAwareZopeTraverse(object, path_items, econtext):
     
         while path_items:
             name = path_items.pop()
    +
    +        if name == '_':
    +            warnings.warn('Traversing to the name `_` is deprecated '
    +                          'and will be removed in Zope 6.',
    +                          DeprecationWarning)
    +        elif name.startswith('_'):
    +            raise NotFound(name)
    +
             if OFS.interfaces.ITraversable.providedBy(object):
                 object = object.restrictedTraverse(name)
             else:
    
  • src/Products/PageTemplates/tests/input/CheckPathTraverse.html+5 0 added
    @@ -0,0 +1,5 @@
    +<html>
    +<body>
    +   <div tal:content="context/laf"></div>
    +</body>
    +</html>
    
  • src/Products/PageTemplates/tests/output/CheckPathTraverse.html+5 0 added
    @@ -0,0 +1,5 @@
    +<html>
    +<body>
    +   <div>ok</div>
    +</body>
    +</html>
    
  • src/Products/PageTemplates/tests/testChameleonTalesExpressions.py+7 0 modified
    @@ -1,3 +1,5 @@
    +import unittest
    +
     from ..expression import getEngine
     from . import testHTMLTests
     
    @@ -19,3 +21,8 @@ def setUp(self):
         #   expressions (e.g. the ``zope.tales`` ``not`` expression
         #   returns ``int``, that of ``chameleon.tales`` ``bool``
         PREFIX = "CH_"
    +
    +    @unittest.skip('The test in the base class relies on a Zope context with'
    +                   ' the "random" module available in expressions')
    +    def test_underscore_traversal(self):
    +        pass
    
  • src/Products/PageTemplates/tests/testExpressions.py+20 1 modified
    @@ -1,6 +1,8 @@
     import unittest
    +import warnings
     
     from AccessControl import safe_builtins
    +from zExceptions import NotFound
     from zope.component.testing import PlacelessSetup
     
     
    @@ -106,8 +108,12 @@ def test_evaluate_alternative_first_missing(self):
             self.assertTrue(ec.evaluate('x | nothing') is None)
     
         def test_evaluate_dict_key_as_underscore(self):
    +        # Traversing to the name `_` will raise a DeprecationWarning
    +        # because it will go away in Zope 6.
             ec = self._makeContext()
    -        self.assertEqual(ec.evaluate('d/_'), 'under')
    +        with warnings.catch_warnings():
    +            warnings.simplefilter('ignore')
    +            self.assertEqual(ec.evaluate('d/_'), 'under')
     
         def test_evaluate_dict_with_key_from_expansion(self):
             ec = self._makeContext()
    @@ -220,6 +226,19 @@ def test_list_in_path_expr(self):
             ec = self._makeContext()
             self.assertIs(ec.evaluate('nocall: list'), safe_builtins["list"])
     
    +    def test_underscore_traversal(self):
    +        # Prevent traversal to names starting with an underscore (_)
    +        ec = self._makeContext()
    +
    +        with self.assertRaises(NotFound):
    +            ec.evaluate("context/__class__")
    +
    +        with self.assertRaises(NotFound):
    +            ec.evaluate("nocall: random/_itertools/repeat")
    +
    +        with self.assertRaises(NotFound):
    +            ec.evaluate("random/_itertools/repeat/foobar")
    +
     
     class TrustedEngineTests(EngineTestsBase, unittest.TestCase):
     
    
  • src/Products/PageTemplates/tests/testHTMLTests.py+25 0 modified
    @@ -26,6 +26,7 @@
         DefaultUnicodeEncodingConflictResolver
     from Products.PageTemplates.unicodeconflictresolver import \
         PreferredCharsetResolver
    +from zExceptions import NotFound
     from zope.component import provideUtility
     from zope.traversing.adapters import DefaultTraversable
     
    @@ -155,6 +156,15 @@ def testPathNothing(self):
         def testPathAlt(self):
             self.assert_expected(self.folder.t, 'CheckPathAlt.html')
     
    +    def testPathTraverse(self):
    +        # need to perform this test with a "real" folder
    +        from OFS.Folder import Folder
    +        f = self.folder
    +        self.folder = Folder()
    +        self.folder.t, self.folder.laf = f.t, f.laf
    +        self.folder.laf.write('ok')
    +        self.assert_expected(self.folder.t, 'CheckPathTraverse.html')
    +
         def testBatchIteration(self):
             self.assert_expected(self.folder.t, 'CheckBatchIteration.html')
     
    @@ -207,3 +217,18 @@ def test_unicode_conflict_resolution(self):
             provideUtility(PreferredCharsetResolver)
             t = PageTemplate()
             self.assert_expected(t, 'UnicodeResolution.html')
    +
    +    def test_underscore_traversal(self):
    +        t = self.folder.t
    +
    +        t.write('<p tal:define="p context/__class__" />')
    +        with self.assertRaises(NotFound):
    +            t()
    +
    +        t.write('<p tal:define="p nocall: random/_itertools/repeat"/>')
    +        with self.assertRaises(NotFound):
    +            t()
    +
    +        t.write('<p tal:content="random/_itertools/repeat/foobar"/>')
    +        with self.assertRaises(NotFound):
    +            t()
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

9

News mentions

0

No linked articles in our index yet.