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