* Mathias Behrle: " simpleeval: CVE-2026-32640" (Wed, 25 Mar 2026 12:24:27
  +0100):

Hi security team,

please find attached the debdiffs for trixie and bookworm and tell if I can
upload to security-master.

Thanks
Mathias





> Hi teams,
> 
> tracked in  https://bugs.debian.org/1130875
> 
> I just uploaded simpleeval_1.0.7-1 to unstable.
> 
> According to https://security-tracker.debian.org/tracker/CVE-2026-32640
> I will try to provide fixes for bullseye and bookworm as well.
> 
> Cheers
> Mathias
> 
> 
> 



-- 

    Mathias Behrle
    PGP/GnuPG key availabable from any keyserver, ID: 0xD6D09BE48405BBF6
    AC29 7E5C 46B9 D0B6 1C71  7681 D6D0 9BE4 8405 BBF6
diff -Nru simpleeval-1.0.3/debian/changelog simpleeval-1.0.3/debian/changelog
--- simpleeval-1.0.3/debian/changelog	2024-11-06 12:56:33.000000000 +0100
+++ simpleeval-1.0.3/debian/changelog	2026-03-26 11:06:04.000000000 +0100
@@ -1,3 +1,15 @@
+simpleeval (1.0.3-1+deb13u1) trixie-security; urgency=high
+
+  * Add 01_CVE-2026-32640.patch (Closes: #1130875)
+    This patch fixes CVE-2026-32640
+    https://github.com/danthedeckie/simpleeval/pull/171
+    with commits
+    https://github.com/danthedeckie/simpleeval/commit/9cb4a7b99498c173263bd90f77bc185e160fb6b8
+    https://github.com/danthedeckie/simpleeval/commit/1654cbf0219345f707c79664b8657be6b8d23e33
+    https://github.com/danthedeckie/simpleeval/commit/cffa9f68cee54404a2ef43d949a8ae8a3311c503
+
+ -- Mathias Behrle <[email protected]>  Thu, 26 Mar 2026 11:06:04 +0100
+
 simpleeval (1.0.3-1) unstable; urgency=medium
 
   * Adding upstream version 1.0.3.
diff -Nru simpleeval-1.0.3/debian/patches/01_CVE-2026-32640.patch simpleeval-1.0.3/debian/patches/01_CVE-2026-32640.patch
--- simpleeval-1.0.3/debian/patches/01_CVE-2026-32640.patch	1970-01-01 01:00:00.000000000 +0100
+++ simpleeval-1.0.3/debian/patches/01_CVE-2026-32640.patch	2026-03-26 11:01:15.000000000 +0100
@@ -0,0 +1,777 @@
+Remove module access and improve security for CVE-2026-32640
+
+This patch concatenates the relevant commits from
+https://github.com/danthedeckie/simpleeval/pull/171
+namely:
+https://github.com/danthedeckie/simpleeval/commit/9cb4a7b99498c173263bd90f77bc185e160fb6b8
+https://github.com/danthedeckie/simpleeval/commit/1654cbf0219345f707c79664b8657be6b8d23e33
+https://github.com/danthedeckie/simpleeval/commit/cffa9f68cee54404a2ef43d949a8ae8a3311c503
+
+Origin: https://github.com/danthedeckie/simpleeval/pull/171
+Bug: https://github.com/danthedeckie/simpleeval/security/advisories/GHSA-44vg-5wv2-h2hg
+Forwarded: not-needed
+Applied-Upstream: 1.0.5
+
+--- simpleeval-1.0.3.orig/simpleeval.py
++++ simpleeval-1.0.3/simpleeval.py
+@@ -104,9 +104,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
+ 
+ ########################################
+ # Module wide 'globals'
+@@ -135,7 +138,21 @@ DISALLOW_METHODS = [
+ # 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, exec}
++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
+ ):
+@@ -232,6 +249,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:
+ 
+@@ -408,6 +473,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"""
+@@ -442,7 +529,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)
+@@ -601,18 +690,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)
+--- simpleeval-1.0.3.orig/test_simpleeval.py
++++ simpleeval-1.0.3/test_simpleeval.py
+@@ -24,6 +24,7 @@ from simpleeval import (
+     FeatureNotAvailable,
+     FunctionNotDefined,
+     InvalidExpression,
++    ModuleWrapper,
+     NameNotDefined,
+     OperatorNotDefined,
+     SimpleEval,
+@@ -591,6 +592,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"""
+@@ -1251,33 +1773,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, exec]
+-
+-        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, exec]
+-
+-        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()
+-
+     def test_breakout_via_generator(self):
+         # Thanks decorator-factory
+         class Foo:
diff -Nru simpleeval-1.0.3/debian/patches/series simpleeval-1.0.3/debian/patches/series
--- simpleeval-1.0.3/debian/patches/series	1970-01-01 01:00:00.000000000 +0100
+++ simpleeval-1.0.3/debian/patches/series	2026-03-26 10:51:41.000000000 +0100
@@ -0,0 +1 @@
+01_CVE-2026-32640.patch
diff -Nru simpleeval-0.9.12/debian/changelog simpleeval-0.9.12/debian/changelog
--- simpleeval-0.9.12/debian/changelog	2022-09-09 20:29:00.000000000 +0200
+++ simpleeval-0.9.12/debian/changelog	2026-03-26 14:45:00.000000000 +0100
@@ -1,3 +1,16 @@
+simpleeval (0.9.12-1+deb12u1) bookworm-security; urgency=high
+
+  * Add 01_CVE-2026-32640.patch.
+    This patch fixes CVE-2026-32640
+    https://github.com/danthedeckie/simpleeval/pull/171
+    with commits
+    https://github.com/danthedeckie/simpleeval/commit/9cb4a7b99498c173263bd90f77bc185e160fb6b8
+    https://github.com/danthedeckie/simpleeval/commit/1654cbf0219345f707c79664b8657be6b8d23e33
+    https://github.com/danthedeckie/simpleeval/commit/cffa9f68cee54404a2ef43d949a8ae8a3311c503
+  * Add a salsa-ci.yml targeting bookworm.
+
+ -- Mathias Behrle <[email protected]>  Thu, 26 Mar 2026 14:45:00 +0100
+
 simpleeval (0.9.12-1) unstable; urgency=medium
 
   * Merging upstream version 0.9.12.
diff -Nru simpleeval-0.9.12/debian/patches/01_CVE-2026-32640.patch simpleeval-0.9.12/debian/patches/01_CVE-2026-32640.patch
--- simpleeval-0.9.12/debian/patches/01_CVE-2026-32640.patch	1970-01-01 01:00:00.000000000 +0100
+++ simpleeval-0.9.12/debian/patches/01_CVE-2026-32640.patch	2026-03-26 14:25:30.000000000 +0100
@@ -0,0 +1,795 @@
+Remove module access and improve security for CVE-2026-32640
+
+This patch concatenates the relevant commits from
+https://github.com/danthedeckie/simpleeval/pull/171
+namely:
+https://github.com/danthedeckie/simpleeval/commit/9cb4a7b99498c173263bd90f77bc185e160fb6b8
+https://github.com/danthedeckie/simpleeval/commit/1654cbf0219345f707c79664b8657be6b8d23e33
+https://github.com/danthedeckie/simpleeval/commit/cffa9f68cee54404a2ef43d949a8ae8a3311c503
+
+Origin: https://github.com/danthedeckie/simpleeval/pull/171
+Bug: https://github.com/danthedeckie/simpleeval/security/advisories/GHSA-44vg-5wv2-h2hg
+Forwarded: not-needed
+Applied-Upstream: 1.0.5
+
+--- a/simpleeval.py
++++ b/simpleeval.py
+@@ -96,9 +96,12 @@
+ 
+ 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
+ 
+@@ -119,7 +122,22 @@
+ # 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__
+ ):
+@@ -204,6 +222,54 @@
+     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:
+ 
+@@ -398,7 +464,9 @@
+                 "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)
+@@ -418,6 +486,28 @@
+     def _eval_import(self, node):
+         raise FeatureNotAvailable("Sorry, 'import' is not allowed.")
+ 
++    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 _eval_num(node):
+         return node.n
+@@ -543,18 +633,25 @@
+         # 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)
+--- a/test_simpleeval.py
++++ b/test_simpleeval.py
+@@ -23,6 +23,7 @@
+     FeatureNotAvailable,
+     FunctionNotDefined,
+     InvalidExpression,
++    ModuleWrapper,
+     NameNotDefined,
+     SimpleEval,
+     simple_eval,
+@@ -537,6 +538,527 @@
+ 
+             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"""
+@@ -1105,37 +1627,50 @@
+ 
+ 
+ class TestDisallowedFunctions(DRYTest):
+-    def test_functions_are_disallowed_at_init(self):
+-        DISALLOWED = [type, isinstance, eval, getattr, setattr, help, repr, compile, open]
+-        if simpleeval.PYTHON3:
+-            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):
+-                s = 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:
+-            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()
++    def test_breakout_via_generator(self):
++        # Thanks decorator-factory
++        class Foo:
++            def bar(self):
++                yield "Hello, world!"
++
++        # Test the generator does work - also adds the `yield` to codecov...
++        assert list(Foo().bar()) == ["Hello, world!"]
++
++        evil = "foo.bar().gi_frame.f_globals['__builtins__'].exec('raise RuntimeError(\"Oh no\")')"
++
++        with self.assertRaises(FeatureNotAvailable):
++            simple_eval(evil, names={"foo": Foo()})
+ 
+ 
+ @unittest.skipIf(simpleeval.PYTHON3 != True, "Python2 fails - but it's not supported anyway.")
diff -Nru simpleeval-0.9.12/debian/patches/series simpleeval-0.9.12/debian/patches/series
--- simpleeval-0.9.12/debian/patches/series	1970-01-01 01:00:00.000000000 +0100
+++ simpleeval-0.9.12/debian/patches/series	2026-03-26 10:51:41.000000000 +0100
@@ -0,0 +1 @@
+01_CVE-2026-32640.patch
diff -Nru simpleeval-0.9.12/debian/salsa-ci.yml simpleeval-0.9.12/debian/salsa-ci.yml
--- simpleeval-0.9.12/debian/salsa-ci.yml	1970-01-01 01:00:00.000000000 +0100
+++ simpleeval-0.9.12/debian/salsa-ci.yml	2026-03-26 14:34:44.000000000 +0100
@@ -0,0 +1,7 @@
+---
+include:
+  - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/salsa-ci.yml
+  - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/pipeline-jobs.yml
+
+variables:
+  RELEASE: 'bookworm'

Reply via email to