* 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'

