Remote Code Execution via traversal in TAL expressions
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.
| Package | Affected versions | Patched versions |
|---|---|---|
ZopePyPI | < 4.6 | 4.6 |
ZopePyPI | >= 5.0, < 5.2 | 5.2 |
Affected products
1- Range: < 4.6
Patches
11f8456bf1f90Merge pull request from GHSA-5pr9-v234-jw36
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- github.com/advisories/GHSA-5pr9-v234-jw36ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2021-32633ghsaADVISORY
- www.openwall.com/lists/oss-security/2021/05/21/1ghsamailing-listx_refsource_MLISTWEB
- www.openwall.com/lists/oss-security/2021/05/22/1ghsamailing-listx_refsource_MLISTWEB
- cyllective.com/blog/post/plone-authenticated-rce-cve-2021-32633ghsaWEB
- cyllective.com/blog/post/plone-authenticated-rce-cve-2021-32633/mitrex_refsource_MISC
- github.com/pypa/advisory-database/tree/main/vulns/zope/PYSEC-2021-88.yamlghsaWEB
- github.com/zopefoundation/Zope/commit/1f8456bf1f908ea46012537d52bd7e752a532c91ghsax_refsource_MISCWEB
- github.com/zopefoundation/Zope/security/advisories/GHSA-5pr9-v234-jw36ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.