Remote Code Execution via traversal in TAL expressions
Description
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.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
ZopePyPI | >= 5.0, < 5.2.1 | 5.2.1 |
ZopePyPI | < 4.6.1 | 4.6.1 |
Affected products
1- Range: >= 5.0.0, < 5.2.1
Patches
11d897910139eMerge pull request from GHSA-rpcg-f9q6-2mq6
6 files changed · +116 −23
CHANGES.rst+4 −1 modified@@ -11,11 +11,14 @@ https://github.com/zopefoundation/Zope/blob/4.x/CHANGES.rst 5.2.1 (unreleased) ------------------ -- Update to newest compatible versions of dependencies. +- Prevent unauthorized traversal through authorized Python modules in + TAL expressions - Facelift the Zope logo. (`#973 <https://github.com/zopefoundation/Zope/issues/973>`_) +- Update to newest compatible versions of dependencies. + 5.2 (2021-05-21) ----------------
src/OFS/zpt/main.zpt+1 −1 modified@@ -5,7 +5,7 @@ <main class="container-fluid"> <form id="objectItems" name="objectItems" method="post" tal:define="has_order_support python:getattr(here.aq_explicit, 'has_order_support', 0); - sm modules/AccessControl/SecurityManagement/getSecurityManager; + sm modules/AccessControl/getSecurityManager; default_sort python: 'position' if has_order_support else 'id'; skey python:request.get('skey',default_sort); rkey python:request.get('rkey','asc');
src/Products/PageTemplates/expression.py+35 −9 modified@@ -10,6 +10,7 @@ from chameleon.tales import NotExpr from chameleon.tales import StringExpr +from AccessControl.SecurityManagement import getSecurityManager from AccessControl.ZopeGuards import guarded_apply from AccessControl.ZopeGuards import guarded_getattr from AccessControl.ZopeGuards import guarded_getitem @@ -57,24 +58,49 @@ class BoboAwareZopeTraverse: def traverse(cls, base, request, path_items): """See ``zope.app.pagetemplate.engine``.""" + validate = getSecurityManager().validate path_items = list(path_items) path_items.reverse() 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.traverse_method)(name) else: - base = traversePathElement(base, name, path_items, - request=request) + 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) return base
src/Products/PageTemplates/Expressions.py+29 −9 modified@@ -21,6 +21,7 @@ import OFS.interfaces from AccessControl import safe_builtins +from AccessControl.SecurityManagement import getSecurityManager from Acquisition import aq_base from MultiMapping import MultiMapping from zExceptions import NotFound @@ -70,24 +71,43 @@ def boboAwareZopeTraverse(object, path_items, econtext): necessary (bobo-awareness). """ request = getattr(econtext, 'request', None) + validate = getSecurityManager().validate path_items = list(path_items) path_items.reverse() 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: - object = traversePathElement(object, name, path_items, - request=request) + 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) + return object
src/Products/PageTemplates/tests/testExpressions.py+3 −2 modified@@ -4,6 +4,7 @@ from AccessControl import safe_builtins from zExceptions import NotFound from zope.component.testing import PlacelessSetup +from zope.location.interfaces import LocationError class EngineTestsBase(PlacelessSetup): @@ -233,10 +234,10 @@ def test_underscore_traversal(self): with self.assertRaises(NotFound): ec.evaluate("context/__class__") - with self.assertRaises(NotFound): + with self.assertRaises((NotFound, LocationError)): ec.evaluate("nocall: random/_itertools/repeat") - with self.assertRaises(NotFound): + with self.assertRaises((NotFound, LocationError)): ec.evaluate("random/_itertools/repeat/foobar")
src/Products/PageTemplates/tests/testHTMLTests.py+44 −1 modified@@ -26,8 +26,10 @@ DefaultUnicodeEncodingConflictResolver from Products.PageTemplates.unicodeconflictresolver import \ PreferredCharsetResolver +from Products.PageTemplates.ZopePageTemplate import ZopePageTemplate from zExceptions import NotFound from zope.component import provideUtility +from zope.location.interfaces import LocationError from zope.traversing.adapters import DefaultTraversable from .util import useChameleonEngine @@ -37,6 +39,10 @@ class AqPageTemplate(Implicit, PageTemplate): pass +class AqZopePageTemplate(Implicit, ZopePageTemplate): + pass + + class Folder(util.Base): pass @@ -74,6 +80,7 @@ def setUp(self): self.folder = f = Folder() f.laf = AqPageTemplate() f.t = AqPageTemplate() + f.z = AqZopePageTemplate('testing') self.policy = UnitTestSecurityPolicy() self.oldPolicy = SecurityManager.setSecurityPolicy(self.policy) noSecurityManager() # Use the new policy. @@ -226,9 +233,45 @@ def test_underscore_traversal(self): t() t.write('<p tal:define="p nocall: random/_itertools/repeat"/>') - with self.assertRaises(NotFound): + with self.assertRaises((NotFound, LocationError)): t() t.write('<p tal:content="random/_itertools/repeat/foobar"/>') + 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) 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
8- github.com/advisories/GHSA-rpcg-f9q6-2mq6ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2021-32674ghsaADVISORY
- github.com/pypa/advisory-database/tree/main/vulns/zope/PYSEC-2021-104.yamlghsaWEB
- github.com/zopefoundation/Zope/commit/1d897910139e2c0b11984fc9b78c1da1365bec21ghsax_refsource_MISCWEB
- github.com/zopefoundation/Zope/security/advisories/GHSA-5pr9-v234-jw36ghsax_refsource_MISCWEB
- github.com/zopefoundation/Zope/security/advisories/GHSA-rpcg-f9q6-2mq6ghsax_refsource_CONFIRMWEB
- pypi.org/project/ZopeghsaWEB
- pypi.org/project/Zope/mitrex_refsource_MISC
News mentions
0No linked articles in our index yet.