CVE-2021-32633 (published 2021-05-21T14:15Z)

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.

Severity: MEDIUM
CVSS v2 base score: 6.5 (AV:N/AC:L/Au:S/C:P/I:P/A:P)
CVSS v3 base score: 8.8, HIGH (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-22
cwe_name: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
description: 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.
extended_description: 
            Many file operations are intended to take place within a restricted directory. By using special elements such as ".." and "/" separators, attackers can escape outside of the restricted location to access files or directories that are elsewhere on the system. One of the most common special elements is the "../" sequence, which in most modern operating systems is interpreted as the parent directory of the current location. This is referred to as relative path traversal. Path traversal also covers the use of absolute pathnames such as "/usr/local/bin" to access unexpected files. This is referred to as absolute path traversal.
         
         

Fix information:
repo_url: https://github.com/zopefoundation/Zope
commit_details: repo_url: https://github.com/zopefoundation/Zope
author: Jens Vagelpohl
author_date: 2021-05-21 09:11:02+02:00
msg: Merge pull request from GHSA-5pr9-v234-jw36

* - Prevent traversal to names starting with ``_`` in TAL expressions

* - include the expressions module

* - simplify test

* - lint fix

* - add DeprecationWarnings for traversal to `_`

* Update src/Products/PageTemplates/tests/testChameleonTalesExpressions.py

Co-authored-by: Michael Howitz <mh@gocept.com>

* - add missing import

* - added tests for the ``traverse`` method fix

* - change control flow

Co-authored-by: Michael Howitz <mh@gocept.com>
repository: repo_url: https://github.com/zopefoundation/Zope
repo_name: zopefoundation/Zope
description: Zope is an open-source web application server.
repo_language: Python
owner: zopefoundation
file_changes: filename: CHANGES.rst
change_type: ModificationType.MODIFY
diff: @@ -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>`_)
 

programming_language: Markdown
filename: Expressions.py
change_type: ModificationType.MODIFY
diff: @@ -17,6 +17,7 @@ for Python expressions, string literals, and paths.
 """
 
 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:

programming_language: Python
filename: expression.py
change_type: ModificationType.MODIFY
diff: @@ -1,5 +1,6 @@
 """``chameleon.tales`` expressions."""
 
+import warnings
 from ast import NodeTransformer
 from ast import parse
 
@@ -61,8 +62,16 @@ class BoboAwareZopeTraverse:
 
         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)

programming_language: Python
filename: CheckPathTraverse.html
change_type: ModificationType.ADD
diff: @@ -0,0 +1,5 @@
+<html>
+<body>
+   <div tal:content="context/laf"></div>
+</body>
+</html>

programming_language: HTML
filename: CheckPathTraverse.html
change_type: ModificationType.ADD
diff: @@ -0,0 +1,5 @@
+<html>
+<body>
+   <div>ok</div>
+</body>
+</html>

programming_language: HTML
filename: testChameleonTalesExpressions.py
change_type: ModificationType.MODIFY
diff: @@ -1,3 +1,5 @@
+import unittest
+
 from ..expression import getEngine
 from . import testHTMLTests
 
@@ -19,3 +21,8 @@ class ChameleonTalesExpressionTests(testHTMLTests.HTMLTests):
     #   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

programming_language: Python
filename: testExpressions.py
change_type: ModificationType.MODIFY
diff: @@ -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 @@ class EngineTestsBase(PlacelessSetup):
         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 @@ class UntrustedEngineTests(EngineTestsBase, unittest.TestCase):
         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):
 

programming_language: Python
filename: testHTMLTests.py
change_type: ModificationType.MODIFY
diff: @@ -26,6 +26,7 @@ from Products.PageTemplates.unicodeconflictresolver import \
     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 @@ class HTMLTests(zope.component.testing.PlacelessSetup, unittest.TestCase):
     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 @@ class HTMLTests(zope.component.testing.PlacelessSetup, unittest.TestCase):
         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()

programming_language: Python