File CVE-2026-32640.patch of Package python-simpleeval.19400

From 9cb4a7b99498c173263bd90f77bc185e160fb6b8 Mon Sep 17 00:00:00 2001
From: Daniel Fairhead <daniel@dev.ngo>
Date: Thu, 12 Mar 2026 09:33:17 +0000
Subject: [PATCH 1/5] Add a few additional DISALLOW_FUNCTIONS

---
 simpleeval.py | 17 ++++++++++++++++-
 1 file changed, 16 insertions(+), 1 deletion(-)

Index: simpleeval-0.9.13/simpleeval.py
===================================================================
--- simpleeval-0.9.13.orig/simpleeval.py
+++ simpleeval-0.9.13/simpleeval.py
@@ -57,6 +57,7 @@ Contributors:
 - bozokopic (Bozo Kopic) Memory leak fix
 - daxamin (Dax Amin) Better error for attempting to eval empty string
 - smurfix (Matthias Urlichs) Allow clearing functions / operators / etc completely
+- ByamB4 (Byambadalai) Reported breakout via module & disallowed functions as object attrs
 
 -------------------------------------
 Basic Usage:
@@ -98,9 +99,12 @@ well:
 
 import ast
 import operator as op
+import os
 import sys
+import types
 import warnings
 from random import random
+from typing import Type, Dict, Set, Union, Hashable
 
 PYTHON3 = sys.version_info[0] == 3
 PYTHON35 = PYTHON3 and sys.version_info > (3, 5)
@@ -123,7 +127,7 @@ DISALLOW_METHODS = ["format", "format_ma
 # their functionality is required, then please wrap them up in a safe container.  And think
 # very hard about it first.  And don't say I didn't warn you.
 # builtins is a dict in python >3.6 but a module before
-DISALLOW_FUNCTIONS = {type, isinstance, eval, getattr, setattr, repr, compile, open}
+DISALLOW_FUNCTIONS = {type, isinstance, eval, getattr, setattr, repr, compile, open, exec, globals, locals, os.popen, os.system}
 if hasattr(__builtins__, "help") or (
     hasattr(__builtins__, "__contains__") and "help" in __builtins__  # type: ignore
 ):
@@ -225,6 +229,54 @@ class MultipleExpressions(UserWarning):
     pass
 
 
+# Sentinal used during attr access
+_ATTR_NOT_FOUND = object()
+
+
+class ModuleWrapper:
+    """Wraps a module to safely expose it in expressions.
+
+    By default, modules are not allowed in simpleeval names to prevent
+    accidental or malicious access to dangerous functions. ModuleWrapper
+    allows explicit opt-in to module access while still enforcing
+    restrictions on dangerous methods and functions.
+
+    Example:
+        >>> from simpleeval import SimpleEval, ModuleWrapper
+        >>> import os.path
+        >>> s = SimpleEval(names={'path': ModuleWrapper(os.path)})
+        >>> s.eval('path.exists("/etc/passwd")')  # Works
+    """
+
+    def __init__(self, module, allowed_attrs=None):
+        """
+        Args:
+            module: The module to wrap
+            allowed_attrs: Optional set of allowed attribute names.
+                          If None, all public attributes are allowed
+                          (but still subject to DISALLOW_METHODS checks).
+        """
+        if not isinstance(module, types.ModuleType):
+            raise TypeError(f"ModuleWrapper requires a module, got {type(module)}")
+        self._module = module
+        self._allowed_attrs = allowed_attrs
+
+    def __getattr__(self, name):
+        # Block private/magic attributes
+        if name.startswith("_"):
+            raise FeatureNotAvailable(f"Access to private attribute '{name}' is not allowed")
+
+        # Check if attribute is in disallowed methods list
+        if name in DISALLOW_METHODS:
+            raise FeatureNotAvailable(f"Method '{name}' is not allowed on modules")
+
+        # Check allowed_attrs whitelist if specified
+        if self._allowed_attrs is not None and name not in self._allowed_attrs:
+            raise FeatureNotAvailable(f"Access to '{name}' is not allowed on this wrapped module")
+
+        return getattr(self._module, name)
+
+
 ########################################
 # Default simple functions to include:
 
@@ -403,6 +455,28 @@ class SimpleEval(object):  # pylint: dis
     def __del__(self):
         self.nodes = None
 
+    def _check_disallowed_items(self, item):
+        """Check if item contains disallowed functions or modules.
+        Recursively checks containers (list, dict, tuple).
+        Raises FeatureNotAvailable if forbidden content found.
+        ModuleWrapper instances are allowed (explicit opt-in to module access).
+        """
+        # Allow ModuleWrapper (explicit opt-in to module access)
+        if isinstance(item, ModuleWrapper):
+            return
+
+        if isinstance(item, types.ModuleType):
+            raise FeatureNotAvailable("Sorry, modules are not allowed")
+        if isinstance(item, Hashable) and item in DISALLOW_FUNCTIONS:
+            raise FeatureNotAvailable("This function is forbidden")
+
+        if isinstance(item, (list, tuple)):
+            for element in item:
+                self._check_disallowed_items(element)
+        elif isinstance(item, dict):
+            for value in item.values():
+                self._check_disallowed_items(value)
+
     @staticmethod
     def parse(expr):
         """parse an expression into a node tree"""
@@ -437,7 +511,9 @@ class SimpleEval(object):  # pylint: dis
                 "Sorry, {0} is not available in this " "evaluator".format(type(node).__name__)
             )
 
-        return handler(node)
+        result = handler(node)
+        self._check_disallowed_items(result)
+        return result
 
     def _eval_expr(self, node):
         return self._eval(node.value)
@@ -588,18 +664,25 @@ class SimpleEval(object):  # pylint: dis
         # eval node
         node_evaluated = self._eval(node.value)
 
+        item = _ATTR_NOT_FOUND
+
         # Maybe the base object is an actual object, not just a dict
         try:
-            return getattr(node_evaluated, node.attr)
+            item = getattr(node_evaluated, node.attr)
         except (AttributeError, TypeError):
-            pass
-
-        # TODO: is this a good idea?  Try and look for [x] if .x doesn't work?
-        if self.ATTR_INDEX_FALLBACK:
-            try:
-                return node_evaluated[node.attr]
-            except (KeyError, TypeError):
-                pass
+            # TODO: is this a good idea?  Try and look for [x] if .x doesn't work?
+            if self.ATTR_INDEX_FALLBACK:
+                try:
+                    item = node_evaluated[node.attr]
+                except (KeyError, TypeError):
+                    pass
+
+        if item is not _ATTR_NOT_FOUND:
+            if isinstance(item, types.ModuleType):
+                raise FeatureNotAvailable("Sorry, modules are not allowed in attribute access")
+            if isinstance(item, Hashable) and item in DISALLOW_FUNCTIONS:
+                raise FeatureNotAvailable("This function is forbidden")
+            return item
 
         # If it is neither, raise an exception
         raise AttributeDoesNotExist(node.attr, self.expr)
Index: simpleeval-0.9.13/test_simpleeval.py
===================================================================
--- simpleeval-0.9.13.orig/test_simpleeval.py
+++ simpleeval-0.9.13/test_simpleeval.py
@@ -23,6 +23,7 @@ from simpleeval import (
     FeatureNotAvailable,
     FunctionNotDefined,
     InvalidExpression,
+    ModuleWrapper,
     NameNotDefined,
     OperatorNotDefined,
     SimpleEval,
@@ -590,6 +591,527 @@ class TestTryingToBreakOut(DRYTest):
 
             simpleeval.DISALLOW_PREFIXES = dis
 
+    def test_breakout_via_module_access(self):
+        import os.path
+
+        s = SimpleEval(names={"path": os.path})
+
+        with self.assertRaises(FeatureNotAvailable):
+            s.eval("path.os.popen('id').read()")
+
+    def test_breakout_via_module_access_attr(self):
+        import os.path
+
+        class Foo:
+            p = os.path
+
+        s = SimpleEval(names={"thing": Foo()})
+
+        with self.assertRaises(FeatureNotAvailable):
+            s.eval("thing.p.os.popen('id').read()")
+
+    def test_breakout_via_disallowed_functions_as_attrs(self):
+        class Foo:
+            p = exec
+
+        s = SimpleEval(names={"thing": Foo()})
+
+        with self.assertRaises(FeatureNotAvailable):
+            s.eval("thing.p('exit')")
+
+    def test_breakout_forbidden_function_in_list(self):
+        """Disallowed functions in lists should be blocked"""
+        s = SimpleEval(names={"funcs": [exec, eval]})
+
+        with self.assertRaises(FeatureNotAvailable):
+            s.eval("funcs[0]('exit')")
+
+        with self.assertRaises(FeatureNotAvailable):
+            s.eval("funcs[1]('1+1')")
+
+    def test_breakout_module_in_list(self):
+        """Modules in lists should be blocked"""
+        import os.path
+
+        s = SimpleEval(names={"things": [os.path, os.system]})
+
+        with self.assertRaises(FeatureNotAvailable):
+            s.eval("things[0].os.popen('id').read()")
+
+    def test_breakout_forbidden_function_in_dict_value(self):
+        """Disallowed functions as dict values should be blocked"""
+        s = SimpleEval(names={"funcs": {"bad": exec, "evil": eval}})
+
+        with self.assertRaises(FeatureNotAvailable):
+            s.eval("funcs['bad']('exit')")
+
+        with self.assertRaises(FeatureNotAvailable):
+            s.eval("funcs['evil']('1+1')")
+
+    def test_breakout_module_in_dict_value(self):
+        """Modules as dict values should be blocked"""
+        import os.path
+
+        s = SimpleEval(names={"things": {"p": os.path, "s": os.system}})
+
+        with self.assertRaises(FeatureNotAvailable):
+            s.eval("things['p'].os.popen('id').read()")
+
+    def test_breakout_function_returning_forbidden_function(self):
+        """Functions returning disallowed functions should be blocked"""
+
+        def get_evil():
+            return exec
+
+        s = SimpleEval(names={}, functions={"get_evil": get_evil})
+
+        with self.assertRaises(FeatureNotAvailable):
+            s.eval("get_evil()('exit')")
+
+    def test_breakout_function_returning_module(self):
+        """Functions returning modules should be blocked"""
+        import os.path
+
+        def get_module():
+            return os.path
+
+        s = SimpleEval(names={}, functions={"get_module": get_module})
+
+        with self.assertRaises(FeatureNotAvailable):
+            s.eval("get_module().os.popen('id').read()")
+
+    def test_dunder_all_in_module(self):
+        """__all__ should be blocked (starts with _)"""
+        import os
+
+        s = SimpleEval(names={"os": os})
+
+        with self.assertRaises(FeatureNotAvailable):
+            s.eval("os.__all__")
+
+    def test_dunder_dict_in_module(self):
+        """__dict__ should be blocked (starts with _)"""
+        import os
+
+        s = SimpleEval(names={"os": os})
+
+        with self.assertRaises(FeatureNotAvailable):
+            s.eval("os.__dict__")
+
+    def test_forbidden_method_in_tuple(self):
+        """Disallowed functions in tuples should be blocked"""
+        s = SimpleEval(names={"funcs": (exec, eval)})
+
+        with self.assertRaises(FeatureNotAvailable):
+            s.eval("funcs[0]('exit')")
+
+    def test_module_in_tuple(self):
+        """Modules in tuples should be blocked"""
+        import os
+
+        s = SimpleEval(names={"mods": (os.path, os.system)})
+
+        with self.assertRaises(FeatureNotAvailable):
+            s.eval("mods[0].os.popen('id').read()")
+
+    def test_breakout_via_nested_container_forbidden_func(self):
+        """Disallowed functions nested in containers should be blocked"""
+        s = SimpleEval(names={"data": {"nested": {"funcs": [exec]}}})
+
+        with self.assertRaises(FeatureNotAvailable):
+            s.eval("data['nested']['funcs'][0]('exit')")
+
+    def test_breakout_via_nested_container_module(self):
+        """Modules nested in containers should be blocked"""
+        import os
+
+        s = SimpleEval(names={"data": {"mods": {"p": os.path}}})
+
+        with self.assertRaises(FeatureNotAvailable):
+            s.eval("data['mods']['p'].os.popen('id').read()")
+
+    def test_forbidden_methods_on_allowed_attrs(self):
+        """Disallowed methods listed in DISALLOW_METHODS should be
+        blocked"""
+        s = SimpleEval()
+
+        # format and format_map are in DISALLOW_METHODS
+        with self.assertRaises(FeatureNotAvailable):
+            s.eval("'test {0}'.format")
+
+        with self.assertRaises(FeatureNotAvailable):
+            s.eval("'test'.format_map({0: 'x'})")
+
+        # __mro__ is in DISALLOW_METHODS
+        with self.assertRaises(FeatureNotAvailable):
+            s.eval("'test'.mro")
+
+    def test_function_returning_forbidden_method(self):
+        """Functions returning disallowed methods should be blocked"""
+
+        def get_exec_module():
+            import os
+
+            return os
+
+        s = SimpleEval(names={}, functions={"get_os": get_exec_module})
+
+        with self.assertRaises(FeatureNotAvailable):
+            s.eval("get_os().__name__")
+
+    def test_compound_module_submodule_access(self):
+        """Accessing submodules of a passed module should be blocked"""
+        import os.path
+
+        s = SimpleEval(names={"path": os.path})
+
+        with self.assertRaises(FeatureNotAvailable):
+            s.eval("path.os")
+
+    def test_forbidden_func_via_class_method(self):
+        """Accessing forbidden functions via class methods should be
+        blocked"""
+
+        class Container:
+            @staticmethod
+            def get_exec():
+                return exec
+
+        s = SimpleEval(names={"c": Container()})
+
+        with self.assertRaises(FeatureNotAvailable):
+            s.eval("c.get_exec()('exit')")
+
+    def test_module_via_class_method(self):
+        """Accessing modules via class methods should be blocked"""
+        import os
+
+        class Container:
+            @staticmethod
+            def get_os():
+                return os
+
+        s = SimpleEval(names={"c": Container()})
+
+        with self.assertRaises(FeatureNotAvailable):
+            s.eval("c.get_os().popen('id').read()")
+
+    def test_forbidden_func_via_property(self):
+        """Accessing forbidden functions via properties should be
+        blocked"""
+
+        class Container:
+            @property
+            def evil(self):
+                return exec
+
+        s = SimpleEval(names={"c": Container()})
+
+        with self.assertRaises(FeatureNotAvailable):
+            s.eval("c.evil('exit')")
+
+    def test_module_via_property(self):
+        """Accessing modules via properties should be blocked"""
+        import os
+
+        class Container:
+            @property
+            def mod(self):
+                return os
+
+        s = SimpleEval(names={"c": Container()})
+
+        with self.assertRaises(FeatureNotAvailable):
+            s.eval("c.mod.popen('id').read()")
+
+    def test_forbidden_function_direct_from_names(self):
+        """Forbidden functions passed directly in names should
+        be blocked when accessed"""
+        s = SimpleEval(names={"evil": exec})
+
+        with self.assertRaises(FeatureNotAvailable):
+            s.eval("evil")
+
+    def test_module_direct_from_names(self):
+        """Modules passed directly in names should be blocked
+        when accessed"""
+        import os
+
+        s = SimpleEval(names={"m": os})
+
+        with self.assertRaises(FeatureNotAvailable):
+            s.eval("m")
+
+    def test_forbidden_function_via_callable_name_handler(self):
+        """Forbidden functions from callable name handlers should
+        be blocked"""
+
+        def name_handler(node):
+            if node.id == "evil":
+                return exec
+            raise simpleeval.NameNotDefined(node.id, "")
+
+        s = SimpleEval(names=name_handler)
+
+        with self.assertRaises(FeatureNotAvailable):
+            s.eval("evil")
+
+    def test_module_via_callable_name_handler(self):
+        """Modules from callable name handlers should be blocked"""
+        import os
+
+        def name_handler(node):
+            if node.id == "m":
+                return os
+            raise simpleeval.NameNotDefined(node.id, "")
+
+        s = SimpleEval(names=name_handler)
+
+        with self.assertRaises(FeatureNotAvailable):
+            s.eval("m")
+
+    def test_forbidden_function_passed_to_custom_function(self):
+        """Passing forbidden functions to custom functions should be
+        blocked - they can be executed by the custom function"""
+
+        def evil_caller(func):
+            return func("print('pwned')")
+
+        s = SimpleEval(names={"evil": exec}, functions={"evil_caller": evil_caller})
+
+        with self.assertRaises(FeatureNotAvailable):
+            s.eval("evil_caller(evil)")
+
+    def test_module_passed_to_custom_function(self):
+        """Passing modules to custom functions should be blocked - they
+        can be used by the custom function"""
+        import os
+
+        def os_caller(mod):
+            return mod.system("id")
+
+        s = SimpleEval(names={"m": os}, functions={"os_caller": os_caller})
+
+        with self.assertRaises(FeatureNotAvailable):
+            s.eval("os_caller(m)")
+
+    def test_forbidden_function_in_list_passed_to_custom_function(self):
+        """Forbidden functions in containers passed to custom functions
+        should be blocked"""
+
+        def extract_and_call(items):
+            return items[0]("print('pwned')")
+
+        s = SimpleEval(names={"funcs": [exec, eval]}, functions={"extract": extract_and_call})
+
+        with self.assertRaises(FeatureNotAvailable):
+            s.eval("extract(funcs)")
+
+    def test_module_in_list_passed_to_custom_function(self):
+        """Modules in containers passed to custom functions should be
+        blocked"""
+        import os
+
+        def extract_and_use(items):
+            return items[0].system("id")
+
+        s = SimpleEval(names={"mods": [os.path, os]}, functions={"extract": extract_and_use})
+
+        with self.assertRaises(FeatureNotAvailable):
+            s.eval("extract(mods)")
+
+    def test_forbidden_function_in_dict_passed_to_custom_function(self):
+        """Forbidden functions in dicts passed to custom functions should
+        be blocked"""
+
+        def extract_and_call(d):
+            return d["bad"]("print('pwned')")
+
+        s = SimpleEval(
+            names={"funcs": {"bad": exec, "good": print}}, functions={"extract": extract_and_call}
+        )
+
+        with self.assertRaises(FeatureNotAvailable):
+            s.eval("extract(funcs)")
+
+    def test_module_in_dict_passed_to_custom_function(self):
+        """Modules in dicts passed to custom functions should be blocked"""
+        import os
+
+        def extract_and_use(d):
+            return d["m"].system("id")
+
+        s = SimpleEval(
+            names={"mods": {"m": os, "p": os.path}}, functions={"extract": extract_and_use}
+        )
+
+        with self.assertRaises(FeatureNotAvailable):
+            s.eval("extract(mods)")
+
+
+class TestModuleWrapper(unittest.TestCase):
+    """Test the ModuleWrapper class itself"""
+
+    def test_module_wrapper_requires_module(self):
+        """ModuleWrapper should reject non-module types"""
+        with self.assertRaises(TypeError):
+            ModuleWrapper("not a module")
+
+        with self.assertRaises(TypeError):
+            ModuleWrapper(42)
+
+        with self.assertRaises(TypeError):
+            ModuleWrapper({})
+
+    def test_module_wrapper_allows_valid_module(self):
+        """ModuleWrapper should accept valid modules"""
+        import os.path
+
+        wrapper = ModuleWrapper(os.path)
+        self.assertIsNotNone(wrapper)
+
+    def test_module_wrapper_blocks_private_attrs(self):
+        """ModuleWrapper should block access to private attributes"""
+        import os.path
+
+        wrapper = ModuleWrapper(os.path)
+
+        with self.assertRaises(FeatureNotAvailable):
+            wrapper.__all__
+
+        with self.assertRaises(FeatureNotAvailable):
+            wrapper._internal
+
+    def test_module_wrapper_allows_public_attrs(self):
+        """ModuleWrapper should allow access to public attributes"""
+        import os.path
+
+        wrapper = ModuleWrapper(os.path)
+        # Should not raise
+        _ = wrapper.exists
+
+    def test_module_wrapper_blocks_disallowed_methods(self):
+        """ModuleWrapper should block access to methods in DISALLOW_METHODS"""
+        import os
+
+        wrapper = ModuleWrapper(os)
+
+        with self.assertRaises(FeatureNotAvailable):
+            wrapper.mro
+
+    def test_module_wrapper_with_allowed_attrs_allows_whitelisted(self):
+        """ModuleWrapper with allowed_attrs should allow whitelisted
+        attributes"""
+        import os.path
+
+        wrapper = ModuleWrapper(os.path, allowed_attrs={"exists", "join"})
+
+        # Should not raise
+        _ = wrapper.exists
+        _ = wrapper.join
+
+    def test_module_wrapper_with_allowed_attrs_blocks_non_whitelisted(self):
+        """ModuleWrapper with allowed_attrs should block non-whitelisted
+        attributes"""
+        import os.path
+
+        wrapper = ModuleWrapper(os.path, allowed_attrs={"exists"})
+
+        with self.assertRaises(FeatureNotAvailable):
+            wrapper.join
+
+    def test_module_wrapper_getattr_returns_actual_attribute(self):
+        """ModuleWrapper.__getattr__ should return the actual module
+        attribute"""
+        import os.path
+
+        wrapper = ModuleWrapper(os.path)
+        result = wrapper.exists
+
+        # Should be the actual function
+        self.assertEqual(result, os.path.exists)
+
+
+class TestModuleWrapperAccess(DRYTest):
+    """Test ModuleWrapper integration with SimpleEval"""
+
+    def test_unwrapped_module_blocked(self):
+        """Unwrapped modules in names should be blocked"""
+        import os.path
+
+        s = SimpleEval(names={"path": os.path})
+
+        with self.assertRaises(FeatureNotAvailable):
+            s.eval("path")
+
+    def test_wrapped_module_allowed(self):
+        """ModuleWrapper should allow module access in eval"""
+        import os.path
+
+        s = SimpleEval(names={"path": ModuleWrapper(os.path)})
+
+        result = s.eval("path.exists('/etc/passwd')")
+        self.assertTrue(isinstance(result, bool))
+
+    def test_wrapped_module_private_attrs_blocked(self):
+        """ModuleWrapper should block private attrs in eval"""
+        import os.path
+
+        s = SimpleEval(names={"path": ModuleWrapper(os.path)})
+
+        with self.assertRaises(FeatureNotAvailable):
+            s.eval("path.__all__")
+
+    def test_wrapped_module_with_whitelist(self):
+        """ModuleWrapper with whitelist should allow whitelisted attrs"""
+        import os.path
+
+        s = SimpleEval(names={"path": ModuleWrapper(os.path, allowed_attrs={"exists"})})
+
+        result = s.eval("path.exists('/etc/passwd')")
+        self.assertTrue(isinstance(result, bool))
+
+    def test_wrapped_module_with_whitelist_blocks_others(self):
+        """ModuleWrapper with whitelist should block non-whitelisted
+        attrs"""
+        import os.path
+
+        s = SimpleEval(names={"path": ModuleWrapper(os.path, allowed_attrs={"exists"})})
+
+        with self.assertRaises(FeatureNotAvailable):
+            s.eval("path.join('a', 'b')")
+
+    def test_wrapped_module_passed_to_function(self):
+        """ModuleWrapper can be passed to custom functions"""
+
+        def process_path(path_mod):
+            return path_mod.exists("/etc/passwd")
+
+        import os.path
+
+        s = SimpleEval(names={"path": ModuleWrapper(os.path)}, functions={"process": process_path})
+
+        result = s.eval("process(path)")
+        self.assertTrue(isinstance(result, bool))
+
+    def test_wrapped_module_in_container(self):
+        """ModuleWrapper can be stored in containers"""
+        import os.path
+
+        s = SimpleEval(names={"items": [ModuleWrapper(os.path), 1, 2]})
+
+        result = s.eval("items")
+        self.assertEqual(len(result), 3)
+
+    def test_wrapped_module_in_dict_container(self):
+        """ModuleWrapper can be stored in dicts"""
+        import os.path
+
+        s = SimpleEval(names={"data": {"path": ModuleWrapper(os.path), "value": 42}})
+
+        result = s.eval("data['value']")
+        self.assertEqual(result, 42)
+
 
 class TestCompoundTypes(DRYTest):
     """Test the compound-types edition of the library"""
@@ -1199,40 +1721,37 @@ class TestShortCircuiting(DRYTest):
 
 
 class TestDisallowedFunctions(DRYTest):
-    def test_functions_are_disallowed_at_init(self):
-        DISALLOWED = [type, isinstance, eval, getattr, setattr, help, repr, compile, open]
-        if simpleeval.PYTHON3:
-            # pylint: disable=exec-used
-            exec("DISALLOWED.append(exec)")  # exec is not a function in Python2...
-
-        for f in simpleeval.DISALLOW_FUNCTIONS:
-            assert f in DISALLOWED
+    def test_functions_in_disallowed_functions_list(self):
+        # a bit of double-entry testing. probably pointless.
+        assert simpleeval.DISALLOW_FUNCTIONS.issuperset(
+            {
+                type,
+                isinstance,
+                eval,
+                getattr,
+                setattr,
+                help,
+                repr,
+                compile,
+                open,
+                exec,
+                os.popen,
+                os.system,
+            }
+        )
 
-        for x in DISALLOWED:
+    def test_functions_are_disallowed_at_init(self):
+        for dangerous_function in simpleeval.DISALLOW_FUNCTIONS:
             with self.assertRaises(FeatureNotAvailable):
-                SimpleEval(functions={"foo": x})
+                SimpleEval(functions={"foo": dangerous_function})
 
     def test_functions_are_disallowed_in_expressions(self):
-        DISALLOWED = [type, isinstance, eval, getattr, setattr, help, repr, compile, open]
-
-        if simpleeval.PYTHON3:
-            # pylint: disable=exec-used
-            exec("DISALLOWED.append(exec)")  # exec is not a function in Python2...
-
-        for f in simpleeval.DISALLOW_FUNCTIONS:
-            assert f in DISALLOWED
-
-        DF = simpleeval.DEFAULT_FUNCTIONS.copy()
-
-        for x in DISALLOWED:
-            simpleeval.DEFAULT_FUNCTIONS = DF.copy()
+        for dangerous_function in simpleeval.DISALLOW_FUNCTIONS:
             with self.assertRaises(FeatureNotAvailable):
                 s = SimpleEval()
-                s.functions["foo"] = x
+                s.functions["foo"] = dangerous_function
                 s.eval("foo(42)")
 
-        simpleeval.DEFAULT_FUNCTIONS = DF.copy()
-
 
 @unittest.skipIf(simpleeval.PYTHON3 is not True, "Python2 fails - but it's not supported anyway.")
 @unittest.skipIf(platform.python_implementation() == "PyPy", "GC set_debug not available in PyPy")
Index: simpleeval-0.9.13/README.rst
===================================================================
--- simpleeval-0.9.13.orig/README.rst
+++ simpleeval-0.9.13/README.rst
@@ -274,6 +274,48 @@ are called 'names'.
 
 You can also hand the handling of names over to a function, if you prefer:
 
+Module Access
+-------------
+
+By default, module access is not allowed in simpleeval to prevent accidental or
+malicious access to dangerous functions. However, if you need to expose modules,
+(eg. `numpy` or similar) you can use ``ModuleWrapper`` to do so safely.
+
+``ModuleWrapper`` allows explicit opt-in to module access while still enforcing
+restrictions on dangerous methods and private attributes:
+
+.. code-block:: pycon
+
+    >>> from simpleeval import SimpleEval, ModuleWrapper
+    >>> import os.path
+    >>> s = SimpleEval(names={'path': ModuleWrapper(os.path)})
+    >>> s.eval("path.exists('/etc/passwd')")
+    True
+
+You can also restrict which attributes are accessible by passing an
+``allowed_attrs`` set:
+
+.. code-block:: pycon
+
+    >>> s = SimpleEval(names={
+    ...     'path': ModuleWrapper(os.path, allowed_attrs={'exists', 'join'})
+    ... })
+    >>> s.eval("path.exists('/etc/passwd')")
+    True
+    >>> s.eval("path.dirname('/etc/passwd')")  # Not in allowed_attrs
+    simpleeval.FeatureNotAvailable: Access to 'dirname' is not allowed...
+
+Private attributes (starting with ``_``) and methods in ``DISALLOW_METHODS``
+are always blocked, even if not using an allowlist:
+
+.. code-block:: pycon
+
+    >>> s = SimpleEval(names={'path': ModuleWrapper(os.path)})
+    >>> s.eval("path.__file__")
+    simpleeval.FeatureNotAvailable: Access to private attribute '__file__'...
+
+If you really really need that - you can make your own wrappers and overrides.
+But I advise against it.
 
 .. code-block:: python
 
@@ -416,6 +458,8 @@ A few builtin functions are listed in ``
 If you need to give access to this kind of functionality to your expressions, then be very
 careful.  You'd be better wrapping the functions in your own safe wrappers.
 
+Accessing modules as attributes is disallowed too.
+
 The initial idea came from J.F. Sebastian on Stack Overflow
 ( http://stackoverflow.com/a/9558001/1973500 ) with modifications and many improvements,
 see the head of the main file for contributors list.
openSUSE Build Service is sponsored by