From f4429eb3491c9c1406f7af8f78f3afbe40a1d204 Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Sat, 6 Jun 2026 00:54:53 +0200 Subject: [PATCH] Fix guarded_getattr ImplPython/ImplC assertion-key parity The pure-Python ``guarded_getattr`` keyed the container assertion on ``type(value.__self__)`` while the C implementation keys on ``type(inst)``. As a result, under ``PURE_PYTHON`` a bound method whose ``__self__`` is an allow-listed simple type (``tuple``/``bytes``/``range``/...) took the truthy-assertion short-circuit and was returned without calling ``validate()``, diverging from the C engine. Key the assertion lookup on ``type(inst)`` to match ``cAccessControl``, and add a regression test (red under ``PURE_PYTHON`` before the fix, green after; the C engine was already correct). Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGES.rst | 8 ++++++++ src/AccessControl/ImplPython.py | 14 ++++++++------ src/AccessControl/tests/testZopeGuards.py | 19 +++++++++++++++++++ 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 8c03534..b600f51 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,6 +7,14 @@ For changes before version 3.0, see ``HISTORY.rst``. 7.4 (unreleased) ---------------- +- Fix a parity defect between the Python and C implementations of + ``guarded_getattr``: the pure-Python implementation keyed the container + assertion on ``type(value.__self__)`` instead of ``type(inst)``, so under + ``PURE_PYTHON`` a bound method of an allow-listed simple type (e.g. + ``tuple``/``bytes``/``range``) was returned without calling ``validate()``. + The Python implementation now keys on ``type(inst)``, matching the C + implementation, and a regression test guards the behaviour. + - Add preliminary support for Python 3.15 as of 3.15b1. - Add support for automatically building and publishing Windows/ARM64 wheels. diff --git a/src/AccessControl/ImplPython.py b/src/AccessControl/ImplPython.py index 0a9326b..e87f1c5 100644 --- a/src/AccessControl/ImplPython.py +++ b/src/AccessControl/ImplPython.py @@ -733,12 +733,14 @@ def guarded_getattr(inst, name, default=_marker): return default raise - try: - container = v.__self__ - except AttributeError: - container = aq_parent(aq_inner(v)) or inst - - assertion = Containers(type(container)) + # The container assertion must be keyed on the type of the accessed + # object, to match the C implementation (see ``cAccessControl.c``, which + # uses ``Containers(type(inst))``). Keying on ``type(v.__self__)`` + # instead let a bound method whose ``__self__`` is an allow-listed simple + # type (e.g. ``tuple``, ``bytes``, ``range``) take the truthy-assertion + # short-circuit and be returned without calling ``validate()`` -- a + # divergence from the C engine. + assertion = Containers(type(inst)) if isinstance(assertion, dict): # We got a table that lets us reason about individual diff --git a/src/AccessControl/tests/testZopeGuards.py b/src/AccessControl/tests/testZopeGuards.py index 5dde02e..982a316 100644 --- a/src/AccessControl/tests/testZopeGuards.py +++ b/src/AccessControl/tests/testZopeGuards.py @@ -119,6 +119,25 @@ def test_calls_validate_for_unknown_type(self): guarded_getattr(self, 'test_calls_validate_for_unknown_type') self.assertEqual(len(self.__sm.calls), 1) + def test_validates_bound_method_of_allow_listed_type(self): + # A bound method whose ``__self__`` is an allow-listed simple type + # (here a ``tuple``), accessed as an attribute of an object that has + # no container assertion of its own, must still be routed through + # ``validate()``. The assertion lookup keys on the type of the + # accessed object (``type(inst)``), not on ``type(value.__self__)`` -- + # otherwise the pure-Python engine short-circuits and diverges from + # the C engine, returning the method without a permission check. + from AccessControl import Unauthorized + from AccessControl.ZopeGuards import guarded_getattr + + class Protected: + feed = (1, 2, 3).count # bound method of an allow-listed tuple + + obj = Protected() + self.__sm.reject = True + self.assertRaises(Unauthorized, guarded_getattr, obj, 'feed') + self.assertEqual(len(self.__sm.calls), 1) + def test_attr_handler_table(self): from AccessControl import Unauthorized from AccessControl.SimpleObjectPolicies import ContainerAssertions