CVE-2023-41050: Exposure of Sensitive Information to an Unauthorized Actor (CWE-200)
value: AccessControl provides a general security framework for use in Zope. Python's "format" functionality allows someone controlling the format string to "read" objects accessible (recursively) via attribute access and subscription from accessible objects. Those attribute accesses and subscriptions use Python's full blown `getattr` and `getitem`, not the policy restricted `AccessControl` variants `_getattr_` and `_getitem_`. This can lead to critical information disclosure. `AccessControl` already provides a safe variant for `str.format` and denies access to `string.Formatter`. However, `str.format_map` is still unsafe. Affected are all users who allow untrusted users to create `AccessControl` controlled Python code and execute it. A fix has been introduced in versions 4.4, 5.8 and 6.2. Users are advised to upgrade. There are no known workarounds for this vulnerability.
CWE description: The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information.
CVSS v3 base score: 7.7
Published: 2023-09-06T18:15Z
Repository: https://github.com/zopefoundation/AccessControl
Language: Python
Files changed: CHANGES.rst, src/AccessControl/__init__.py, src/AccessControl/safe_formatter.py, src/AccessControl/tests/test_safe_formatter.py
Commit date: 2023-09-04 07:28:15 +0200
Commit message: Merge pull request from GHSA-8xv7-89vj-q48c

* fix information disclosure via `format_map`

* Fix CHANGES.rst according to review

Co-authored-by: Jens Vagelpohl <jens@plyp.com>

* Add CVE

---------

Co-authored-by: Jens Vagelpohl <jens@plyp.com>
Co-authored-by: Michael Howitz <mh@gocept.com>
Diff: diff --git a/CHANGES.rst b/CHANGES.rst
index 4514bf7..d6ec6a6 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -4,10 +4,13 @@ Changelog
 For changes before version 3.0, see ``HISTORY.rst``.
 
 6.2 (unreleased)
 ----------------
 
+- Fix information disclosure through ``str.format_map``.
+  (CVE-2023-41050)
+
 
 6.1 (2023-05-22)
 ----------------
 - Update C header files for ``ExtensionClass`` and ``Acquisition``
   from the original packages where needed.
diff --git a/src/AccessControl/__init__.py b/src/AccessControl/__init__.py
index afba2a9..a9597d3 100644
--- a/src/AccessControl/__init__.py
+++ b/src/AccessControl/__init__.py
@@ -12,10 +12,11 @@
 ##############################################################################
 
 # This has to happen early so things get initialized properly
 from AccessControl.Implementation import setImplementation
 from AccessControl.safe_formatter import safe_format
+from AccessControl.safe_formatter import safe_format_map
 from AccessControl.SecurityInfo import ACCESS_NONE
 from AccessControl.SecurityInfo import ACCESS_PRIVATE
 from AccessControl.SecurityInfo import ACCESS_PUBLIC
 from AccessControl.SecurityInfo import ClassSecurityInfo
 from AccessControl.SecurityInfo import ModuleSecurityInfo
@@ -42,10 +43,11 @@ ModuleSecurityInfo('DateTime').declarePublic('DateTime')  # NOQA: D001
 
 # We want to allow all methods on string type except "format".
 # That one needs special handling to avoid access to attributes.
 rules = {m: True for m in dir(str) if not m.startswith('_')}
 rules['format'] = safe_format
+rules['format_map'] = safe_format_map
 allow_type(str, rules)
 
 zodbupdate_decode_dict = {
     'AccessControl.users User name': 'utf-8',
     'AccessControl.users User __': 'utf-8',
diff --git a/src/AccessControl/safe_formatter.py b/src/AccessControl/safe_formatter.py
index e777c97..6128368 100644
--- a/src/AccessControl/safe_formatter.py
+++ b/src/AccessControl/safe_formatter.py
@@ -70,9 +70,17 @@ class SafeFormatter(string.Formatter):
     def safe_format(self, *args, **kwargs):
         """Safe variant of `format` method."""
         kwargs = _MagicFormatMapping(args, kwargs)
         return self.vformat(self.value, args, kwargs)
 
+    def safe_format_map(self, kw):
+        kwargs = _MagicFormatMapping((), kw)
+        return self.vformat(self.value, (), kwargs)
+
 
 def safe_format(inst, method):
     """Use our SafeFormatter that uses guarded_getattr for attribute access."""
     return SafeFormatter(inst).safe_format
+
+
+def safe_format_map(inst, method):
+    return SafeFormatter(inst).safe_format_map
diff --git a/src/AccessControl/tests/test_safe_formatter.py b/src/AccessControl/tests/test_safe_formatter.py
index 68f8408..9eea54b 100644
--- a/src/AccessControl/tests/test_safe_formatter.py
+++ b/src/AccessControl/tests/test_safe_formatter.py
@@ -200,5 +200,27 @@ class FormatterTest(unittest.TestCase):
                          '<Item public2>')
         # But not the private item:
         self.assertRaises(Unauthorized,
                           SafeFormatter('{0[1]}').safe_format,
                           folder)
+
+    def test_format_map(self):
+        from AccessControl.safe_formatter import SafeFormatter
+
+        # Accessing basic Python types in a basic Python list is fine.
+        foo = list(['bar'])
+        self.assertEqual(SafeFormatter('{foo[0]}')
+                         .safe_format_map(dict(foo=foo)),
+                         'bar')
+        # But for non-basic items or non-basic lists, we want run checks.
+        folder = self._create_folder_with_mixed_contents()
+        # We can get the public items just fine:
+        self.assertEqual(SafeFormatter('{foo[0]}')
+                         .safe_format_map(dict(foo=folder)),
+                         '<Item public1>')
+        self.assertEqual(SafeFormatter('{foo[2]}')
+                         .safe_format_map(dict(foo=folder)),
+                         '<Item public2>')
+        # But not the private item:
+        self.assertRaises(Unauthorized,
+                          SafeFormatter('{foo[1]}').safe_format_map,
+                          dict(foo=folder))

Fixed code: - Fix information disclosure through ``str.format_map``.
  (CVE-2023-41050)

from AccessControl.safe_formatter import safe_format_map
rules['format_map'] = safe_format_map
    def safe_format_map(self, kw):
        kwargs = _MagicFormatMapping((), kw)
        return self.vformat(self.value, (), kwargs)



def safe_format_map(inst, method):
    return SafeFormatter(inst).safe_format_map

    def test_format_map(self):
        from AccessControl.safe_formatter import SafeFormatter

        # Accessing basic Python types in a basic Python list is fine.
        foo = list(['bar'])
        self.assertEqual(SafeFormatter('{foo[0]}')
                         .safe_format_map(dict(foo=foo)),
                         'bar')
        # But for non-basic items or non-basic lists, we want run checks.
        folder = self._create_folder_with_mixed_contents()
        # We can get the public items just fine:
        self.assertEqual(SafeFormatter('{foo[0]}')
                         .safe_format_map(dict(foo=folder)),
                         '<Item public1>')
        self.assertEqual(SafeFormatter('{foo[2]}')
                         .safe_format_map(dict(foo=folder)),
                         '<Item public2>')
        # But not the private item:
        self.assertRaises(Unauthorized,
                          SafeFormatter('{foo[1]}').safe_format_map,
                          dict(foo=folder))