CVE-2023-41039: Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection') (CWE-74)
value: RestrictedPython is a restricted execution environment for Python to run untrusted code. Python's "format" functionality allows someone controlling the format string to "read" all objects accessible through recursive attribute lookup and subscription from objects he can access. This can lead to critical information disclosure. With `RestrictedPython`, the format functionality is available via the `format` and `format_map` methods of `str` (and `unicode`) (accessed either via the class or its instances) and via `string.Formatter`. All known versions of `RestrictedPython` are vulnerable. This issue has been addressed in commit `4134aedcff1` which has been included in the 5.4 and 6.2 releases. Users are advised to upgrade. There are no known workarounds for this vulnerability.


CWE description: The product constructs all or part of a command, data structure, or record using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify how it is parsed or interpreted when it is sent to a downstream component.
CVSS v3 base score: 7.7
Published: 2023-08-30T18:15Z
Repository: https://github.com/zopefoundation/RestrictedPython
Language: Python
Files changed: CHANGES.rst, src/RestrictedPython/Guards.py, src/RestrictedPython/Utilities.py, tests/builtins/test_utilities.py, tests/test_Guards.py, tests/test_Utilities.py
Commit date: 2023-08-30 08:28:28 +0200
Commit message: Merge pull request from GHSA-xjw2-6jm9-rf67

* fix information disclosure problems through the `format*` methods of the `str` class and its instances

* Suggestion from @icemac

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

* prevent access to `string.Formatter`

* fix syntax in `CHANGES.rst`

---------

Co-authored-by: Michael Howitz <mh@gocept.com>
Diff: diff --git a/CHANGES.rst b/CHANGES.rst
index b57c2e3..5264c4b 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -20,10 +20,15 @@ Fixes
 
 - Prevent DeprecationWarnings from ``ast.Str`` and ``ast.Num`` on Python 3.12
 
 - Forbid using some attributes providing access to restricted Python internals.
 
+- Fix information disclosure problems through
+  Python's "format" functionality
+  (``format`` and ``format_map`` methods on ``str`` and its instances,
+  ``string.Formatter``).
+
 
 6.0 (2022-11-03)
 ----------------
 
 Backwards incompatible changes
diff --git a/src/RestrictedPython/Guards.py b/src/RestrictedPython/Guards.py
index 9b70ae3..6713bc9 100644
--- a/src/RestrictedPython/Guards.py
+++ b/src/RestrictedPython/Guards.py
@@ -244,13 +244,15 @@ def safer_getattr(object, name, default=None, getattr=getattr):
 
     format() is considered harmful:
     http://lucumr.pocoo.org/2016/12/29/careful-with-str-format/
 
     """
-    if isinstance(object, str) and name == 'format':
+    if name in ('format', 'format_map') and (
+            isinstance(object, str) or
+            (isinstance(object, type) and issubclass(object, str))):
         raise NotImplementedError(
-            'Using format() on a %s is not safe.' % object.__class__.__name__)
+            'Using the format*() methods of `str` is not safe')
     if name.startswith('_'):
         raise AttributeError(
             '"{name}" is an invalid attribute name because it '
             'starts with "_"'.format(name=name)
         )
diff --git a/src/RestrictedPython/Utilities.py b/src/RestrictedPython/Utilities.py
index 4bc6677..10188e9 100644
--- a/src/RestrictedPython/Utilities.py
+++ b/src/RestrictedPython/Utilities.py
@@ -16,11 +16,25 @@ import random
 import string
 
 
 utility_builtins = {}
 
-utility_builtins['string'] = string
+
+class _AttributeDelegator:
+    def __init__(self, mod, *excludes):
+        """delegate attribute lookups outside *excludes* to module *mod*."""
+        self.__mod = mod
+        self.__excludes = excludes
+
+    def __getattr__(self, attr):
+        if attr in self.__excludes:
+            raise NotImplementedError(
+                f"{self.__mod.__name__}.{attr} is not safe")
+        return getattr(self.__mod, attr)
+
+
+utility_builtins['string'] = _AttributeDelegator(string, "Formatter")
 utility_builtins['math'] = math
 utility_builtins['random'] = random
 utility_builtins['whrandom'] = random
 utility_builtins['set'] = set
 utility_builtins['frozenset'] = frozenset
diff --git a/tests/builtins/test_utilities.py b/tests/builtins/test_utilities.py
index 13d7d38..1ad26d7 100644
--- a/tests/builtins/test_utilities.py
+++ b/tests/builtins/test_utilities.py
@@ -3,11 +3,14 @@ import random
 import string
 
 
 def test_string_in_utility_builtins():
     from RestrictedPython.Utilities import utility_builtins
-    assert utility_builtins['string'] is string
+
+    # we no longer provide access to ``string`` itself, only to
+    # a restricted view of it
+    assert utility_builtins['string'].__name__ == string.__name__
 
 
 def test_math_in_utility_builtins():
     from RestrictedPython.Utilities import utility_builtins
     assert utility_builtins['math'] is math
diff --git a/tests/test_Guards.py b/tests/test_Guards.py
index bd61ec7..6200664 100644
--- a/tests/test_Guards.py
+++ b/tests/test_Guards.py
@@ -158,42 +158,82 @@ STRING_DOT_FORMAT_DENIED = """\
 a = 'Hello {}'
 b = a.format('world')
 """
 
 
-def test_Guards__safer_getattr__1():
+def test_Guards__safer_getattr__1a():
     """It prevents using the format method of a string.
 
     format() is considered harmful:
     http://lucumr.pocoo.org/2016/12/29/careful-with-str-format/
     """
     glb = {
         '__builtins__': safe_builtins,
     }
     with pytest.raises(NotImplementedError) as err:
         restricted_exec(STRING_DOT_FORMAT_DENIED, glb)
-    assert 'Using format() on a str is not safe.' == str(err.value)
+    assert 'Using the format*() methods of `str` is not safe' == str(err.value)
 
 
-UNICODE_DOT_FORMAT_DENIED = """\
-a = u'Hello {}'
-b = a.format(u'world')
+# contributed by Ward Theunisse
+STRING_DOT_FORMAT_MAP_DENIED = """\
+a = 'Hello {foo.__dict__}'
+b = a.format_map({foo:str})
 """
 
 
-def test_Guards__safer_getattr__2():
-    """It prevents using the format method of a unicode.
+def test_Guards__safer_getattr__1b():
+    """It prevents using the format method of a string.
+
+    format() is considered harmful:
+    http://lucumr.pocoo.org/2016/12/29/careful-with-str-format/
+    """
+    glb = {
+        '__builtins__': safe_builtins,
+    }
+    with pytest.raises(NotImplementedError) as err:
+        restricted_exec(STRING_DOT_FORMAT_MAP_DENIED, glb)
+    assert 'Using the format*() methods of `str` is not safe' == str(err.value)
+
+
+# contributed by Abhishek Govindarasu
+STR_DOT_FORMAT_DENIED = """\
+str.format('{0.__class__.__mro__[1]}', int)
+"""
+
+
+def test_Guards__safer_getattr__1c():
+    """It prevents using the format method of a string.
+
+    format() is considered harmful:
+    http://lucumr.pocoo.org/2016/12/29/careful-with-str-format/
+    """
+    glb = {
+        '__builtins__': safe_builtins,
+    }
+    with pytest.raises(NotImplementedError) as err:
+        restricted_exec(STR_DOT_FORMAT_DENIED, glb)
+    assert 'Using the format*() methods of `str` is not safe' == str(err.value)
+
+
+STR_DOT_FORMAT_MAP_DENIED = """\
+str.format_map('Hello {foo.__dict__}', {'foo':str})
+"""
+
+
+def test_Guards__safer_getattr__1d():
+    """It prevents using the format method of a string.
 
     format() is considered harmful:
     http://lucumr.pocoo.org/2016/12/29/careful-with-str-format/
     """
     glb = {
         '__builtins__': safe_builtins,
     }
     with pytest.raises(NotImplementedError) as err:
-        restricted_exec(UNICODE_DOT_FORMAT_DENIED, glb)
-    assert 'Using format() on a str is not safe.' == str(err.value)
+        restricted_exec(STR_DOT_FORMAT_MAP_DENIED, glb)
+    assert 'Using the format*() methods of `str` is not safe' == str(err.value)
 
 
 SAFER_GETATTR_ALLOWED = """\
 class A:
 
diff --git a/tests/test_Utilities.py b/tests/test_Utilities.py
index ba9a858..ea50e44 100644
--- a/tests/test_Utilities.py
+++ b/tests/test_Utilities.py
@@ -1,7 +1,10 @@
+import pytest
+
 from RestrictedPython.Utilities import reorder
 from RestrictedPython.Utilities import test
+from RestrictedPython.Utilities import utility_builtins
 
 
 def test_Utilities__test_1():
     """It returns the first arg after the first argument which is True"""
     assert test(True, 1, False, 2) == 1
@@ -28,5 +31,15 @@ def test_Utilities__reorder_1():
     """It also supports 2-tuples containing key, value."""
     s = [('k1', 'v1'), ('k2', 'v2'), ('k3', 'v3')]
     _with = [('k2', 'v2'), ('k3', 'v3')]
     without = [('k2', 'v2'), ('k4', 'v4')]
     assert reorder(s, _with, without) == [('k3', 'v3')]
+
+
+def test_Utilities_string_Formatter():
+    """Access to ``string.Formatter`` is denied."""
+    string = utility_builtins["string"]
+    # access successful in principle
+    assert string.ascii_lowercase == 'abcdefghijklmnopqrstuvwxyz'
+    with pytest.raises(NotImplementedError) as exc:
+        string.Formatter
+    assert 'string.Formatter is not safe' == str(exc.value)

Vulnerable code:     if isinstance(object, str) and name == 'format':
            'Using format() on a %s is not safe.' % object.__class__.__name__)
utility_builtins['string'] = string
    assert utility_builtins['string'] is string
def test_Guards__safer_getattr__1():
    assert 'Using format() on a str is not safe.' == str(err.value)
UNICODE_DOT_FORMAT_DENIED = """\
a = u'Hello {}'
b = a.format(u'world')
def test_Guards__safer_getattr__2():
    """It prevents using the format method of a unicode.
        restricted_exec(UNICODE_DOT_FORMAT_DENIED, glb)
    assert 'Using format() on a str is not safe.' == str(err.value)
Fixed code: - Fix information disclosure problems through
  Python's "format" functionality
  (``format`` and ``format_map`` methods on ``str`` and its instances,
  ``string.Formatter``).

    if name in ('format', 'format_map') and (
            isinstance(object, str) or
            (isinstance(object, type) and issubclass(object, str))):
            'Using the format*() methods of `str` is not safe')

class _AttributeDelegator:
    def __init__(self, mod, *excludes):
        """delegate attribute lookups outside *excludes* to module *mod*."""
        self.__mod = mod
        self.__excludes = excludes

    def __getattr__(self, attr):
        if attr in self.__excludes:
            raise NotImplementedError(
                f"{self.__mod.__name__}.{attr} is not safe")
        return getattr(self.__mod, attr)


utility_builtins['string'] = _AttributeDelegator(string, "Formatter")

    # we no longer provide access to ``string`` itself, only to
    # a restricted view of it
    assert utility_builtins['string'].__name__ == string.__name__
def test_Guards__safer_getattr__1a():
    assert 'Using the format*() methods of `str` is not safe' == str(err.value)
# contributed by Ward Theunisse
STRING_DOT_FORMAT_MAP_DENIED = """\
a = 'Hello {foo.__dict__}'
b = a.format_map({foo:str})
def test_Guards__safer_getattr__1b():
    """It prevents using the format method of a string.

    format() is considered harmful:
    http://lucumr.pocoo.org/2016/12/29/careful-with-str-format/
    """
    glb = {
        '__builtins__': safe_builtins,
    }
    with pytest.raises(NotImplementedError) as err:
        restricted_exec(STRING_DOT_FORMAT_MAP_DENIED, glb)
    assert 'Using the format*() methods of `str` is not safe' == str(err.value)


# contributed by Abhishek Govindarasu
STR_DOT_FORMAT_DENIED = """\
str.format('{0.__class__.__mro__[1]}', int)
"""


def test_Guards__safer_getattr__1c():
    """It prevents using the format method of a string.

    format() is considered harmful:
    http://lucumr.pocoo.org/2016/12/29/careful-with-str-format/
    """
    glb = {
        '__builtins__': safe_builtins,
    }
    with pytest.raises(NotImplementedError) as err:
        restricted_exec(STR_DOT_FORMAT_DENIED, glb)
    assert 'Using the format*() methods of `str` is not safe' == str(err.value)


STR_DOT_FORMAT_MAP_DENIED = """\
str.format_map('Hello {foo.__dict__}', {'foo':str})
"""


def test_Guards__safer_getattr__1d():
    """It prevents using the format method of a string.
        restricted_exec(STR_DOT_FORMAT_MAP_DENIED, glb)
    assert 'Using the format*() methods of `str` is not safe' == str(err.value)
import pytest

from RestrictedPython.Utilities import utility_builtins


def test_Utilities_string_Formatter():
    """Access to ``string.Formatter`` is denied."""
    string = utility_builtins["string"]
    # access successful in principle
    assert string.ascii_lowercase == 'abcdefghijklmnopqrstuvwxyz'
    with pytest.raises(NotImplementedError) as exc:
        string.Formatter
    assert 'string.Formatter is not safe' == str(exc.value)