@@ -390,6 +390,50 @@ class MultipleExpressions(UserWarning):
390390_ATTR_NOT_FOUND = object ()
391391
392392
393+ class ModuleWrapper :
394+ """Wraps a module to safely expose it in expressions.
395+
396+ By default, modules are not allowed in simpleeval names to prevent
397+ accidental or malicious access to dangerous functions. ModuleWrapper
398+ allows explicit opt-in to module access while still enforcing
399+ restrictions on dangerous methods and functions.
400+
401+ Example:
402+ >>> from simpleeval import SimpleEval, ModuleWrapper
403+ >>> import os.path
404+ >>> s = SimpleEval(names={'path': ModuleWrapper(os.path)})
405+ >>> s.eval('path.exists("/etc/passwd")') # Works
406+ """
407+
408+ def __init__ (self , module , allowed_attrs = None ):
409+ """
410+ Args:
411+ module: The module to wrap
412+ allowed_attrs: Optional set of allowed attribute names.
413+ If None, all public attributes are allowed
414+ (but still subject to DISALLOW_METHODS checks).
415+ """
416+ if not isinstance (module , types .ModuleType ):
417+ raise TypeError (f"ModuleWrapper requires a module, got { type (module )} " )
418+ self ._module = module
419+ self ._allowed_attrs = allowed_attrs
420+
421+ def __getattr__ (self , name ):
422+ # Block private/magic attributes
423+ if name .startswith ("_" ):
424+ raise FeatureNotAvailable (f"Access to private attribute '{ name } ' is not allowed" )
425+
426+ # Check if attribute is in disallowed methods list
427+ if name in DISALLOW_METHODS :
428+ raise FeatureNotAvailable (f"Method '{ name } ' is not allowed on modules" )
429+
430+ # Check allowed_attrs whitelist if specified
431+ if self ._allowed_attrs is not None and name not in self ._allowed_attrs :
432+ raise FeatureNotAvailable (f"Access to '{ name } ' is not allowed on this wrapped module" )
433+
434+ return getattr (self ._module , name )
435+
436+
393437########################################
394438# Default simple functions to include:
395439
@@ -567,6 +611,28 @@ def __init__(self, operators=None, functions=None, names=None, allowed_attrs=Non
567611 def __del__ (self ):
568612 self .nodes = None
569613
614+ def _check_disallowed_items (self , item ):
615+ """Check if item contains disallowed functions or modules.
616+ Recursively checks containers (list, dict, tuple).
617+ Raises FeatureNotAvailable if forbidden content found.
618+ ModuleWrapper instances are allowed (explicit opt-in to module access).
619+ """
620+ # Allow ModuleWrapper (explicit opt-in to module access)
621+ if isinstance (item , ModuleWrapper ):
622+ return
623+
624+ if isinstance (item , types .ModuleType ):
625+ raise FeatureNotAvailable ("Sorry, modules are not allowed" )
626+ if isinstance (item , Hashable ) and item in DISALLOW_FUNCTIONS :
627+ raise FeatureNotAvailable ("This function is forbidden" )
628+
629+ if isinstance (item , (list , tuple )):
630+ for element in item :
631+ self ._check_disallowed_items (element )
632+ elif isinstance (item , dict ):
633+ for value in item .values ():
634+ self ._check_disallowed_items (value )
635+
570636 @staticmethod
571637 def parse (expr ):
572638 """parse an expression into a node tree"""
@@ -601,7 +667,9 @@ def _eval(self, node):
601667 "Sorry, {0} is not available in this evaluator" .format (type (node ).__name__ )
602668 )
603669
604- return handler (node )
670+ result = handler (node )
671+ self ._check_disallowed_items (result )
672+ return result
605673
606674 def _eval_expr (self , node ):
607675 return self ._eval (node .value )
0 commit comments