simpleeval 1.0.4__tar.gz → 1.0.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: simpleeval
3
- Version: 1.0.4
3
+ Version: 1.0.5
4
4
  Summary: A simple, safe single expression evaluator library.
5
5
  Project-URL: Source code, https://github.com/danthedeckie/simpleeval
6
6
  Author-email: Daniel Fairhead <danthedeckie@gmail.com>
@@ -460,6 +460,11 @@ A few builtin functions are listed in ``simpleeval.DISALLOW_FUNCTIONS``. ``type
460
460
  If you need to give access to this kind of functionality to your expressions, then be very
461
461
  careful. You'd be better wrapping the functions in your own safe wrappers.
462
462
 
463
+ Accessing modules as attributes is disallowed too.
464
+
465
+ Allowlist recommendation
466
+ ------------------------
467
+
463
468
  There is an additional layer of protection you can add in by passing in ``allowed_attrs``, which
464
469
  makes all attribute access based opt-in rather than opt-out - which is a lot safer design:
465
470
 
@@ -477,8 +482,8 @@ reasonably sensible defaults with BASIC_ALLOWED_ATTRS:
477
482
 
478
483
  is fine - ``strip()`` should be safe on strings.
479
484
 
480
- It is recommended to add ``allowed_attrs=BASIC_ALLOWED_ATTRS`` whenever possible, and it will
481
- be the default for 2.x.
485
+ It is strongly recommended to add ``allowed_attrs=BASIC_ALLOWED_ATTRS`` whenever possible,
486
+ and it will be the default for 2.x.
482
487
 
483
488
  You can add your own classes & limit access to attrs:
484
489
 
@@ -499,6 +504,48 @@ You can add your own classes & limit access to attrs:
499
504
 
500
505
  will now allow access to `foo.bar` but not allow anything else.
501
506
 
507
+ Module Access
508
+ -------------
509
+
510
+ By default, module access is not allowed in simpleeval to prevent accidental or
511
+ malicious access to dangerous functions. However, if you need to expose modules,
512
+ (eg. `numpy` or similar) you can use ``ModuleWrapper`` to do so safely.
513
+
514
+ ``ModuleWrapper`` allows explicit opt-in to module access while still enforcing
515
+ restrictions on dangerous methods and private attributes:
516
+
517
+ .. code-block:: pycon
518
+
519
+ >>> from simpleeval import SimpleEval, ModuleWrapper
520
+ >>> import os.path
521
+ >>> s = SimpleEval(names={'path': ModuleWrapper(os.path)})
522
+ >>> s.eval("path.exists('/etc/passwd')")
523
+ True
524
+
525
+ You can also restrict which attributes are accessible by passing an
526
+ ``allowed_attrs`` set:
527
+
528
+ .. code-block:: pycon
529
+
530
+ >>> s = SimpleEval(names={
531
+ ... 'path': ModuleWrapper(os.path, allowed_attrs={'exists', 'join'})
532
+ ... })
533
+ >>> s.eval("path.exists('/etc/passwd')")
534
+ True
535
+ >>> s.eval("path.dirname('/etc/passwd')") # Not in allowed_attrs
536
+ simpleeval.FeatureNotAvailable: Access to 'dirname' is not allowed...
537
+
538
+ Private attributes (starting with ``_``) and methods in ``DISALLOW_METHODS``
539
+ are always blocked, even if not using an allowlist:
540
+
541
+ .. code-block:: pycon
542
+
543
+ >>> s = SimpleEval(names={'path': ModuleWrapper(os.path)})
544
+ >>> s.eval("path.__file__")
545
+ simpleeval.FeatureNotAvailable: Access to private attribute '__file__'...
546
+
547
+ If you really really need that - you can make your own wrappers and overrides.
548
+ But I advise against it.
502
549
 
503
550
  Other...
504
551
  --------
@@ -443,6 +443,11 @@ A few builtin functions are listed in ``simpleeval.DISALLOW_FUNCTIONS``. ``type
443
443
  If you need to give access to this kind of functionality to your expressions, then be very
444
444
  careful. You'd be better wrapping the functions in your own safe wrappers.
445
445
 
446
+ Accessing modules as attributes is disallowed too.
447
+
448
+ Allowlist recommendation
449
+ ------------------------
450
+
446
451
  There is an additional layer of protection you can add in by passing in ``allowed_attrs``, which
447
452
  makes all attribute access based opt-in rather than opt-out - which is a lot safer design:
448
453
 
@@ -460,8 +465,8 @@ reasonably sensible defaults with BASIC_ALLOWED_ATTRS:
460
465
 
461
466
  is fine - ``strip()`` should be safe on strings.
462
467
 
463
- It is recommended to add ``allowed_attrs=BASIC_ALLOWED_ATTRS`` whenever possible, and it will
464
- be the default for 2.x.
468
+ It is strongly recommended to add ``allowed_attrs=BASIC_ALLOWED_ATTRS`` whenever possible,
469
+ and it will be the default for 2.x.
465
470
 
466
471
  You can add your own classes & limit access to attrs:
467
472
 
@@ -482,6 +487,48 @@ You can add your own classes & limit access to attrs:
482
487
 
483
488
  will now allow access to `foo.bar` but not allow anything else.
484
489
 
490
+ Module Access
491
+ -------------
492
+
493
+ By default, module access is not allowed in simpleeval to prevent accidental or
494
+ malicious access to dangerous functions. However, if you need to expose modules,
495
+ (eg. `numpy` or similar) you can use ``ModuleWrapper`` to do so safely.
496
+
497
+ ``ModuleWrapper`` allows explicit opt-in to module access while still enforcing
498
+ restrictions on dangerous methods and private attributes:
499
+
500
+ .. code-block:: pycon
501
+
502
+ >>> from simpleeval import SimpleEval, ModuleWrapper
503
+ >>> import os.path
504
+ >>> s = SimpleEval(names={'path': ModuleWrapper(os.path)})
505
+ >>> s.eval("path.exists('/etc/passwd')")
506
+ True
507
+
508
+ You can also restrict which attributes are accessible by passing an
509
+ ``allowed_attrs`` set:
510
+
511
+ .. code-block:: pycon
512
+
513
+ >>> s = SimpleEval(names={
514
+ ... 'path': ModuleWrapper(os.path, allowed_attrs={'exists', 'join'})
515
+ ... })
516
+ >>> s.eval("path.exists('/etc/passwd')")
517
+ True
518
+ >>> s.eval("path.dirname('/etc/passwd')") # Not in allowed_attrs
519
+ simpleeval.FeatureNotAvailable: Access to 'dirname' is not allowed...
520
+
521
+ Private attributes (starting with ``_``) and methods in ``DISALLOW_METHODS``
522
+ are always blocked, even if not using an allowlist:
523
+
524
+ .. code-block:: pycon
525
+
526
+ >>> s = SimpleEval(names={'path': ModuleWrapper(os.path)})
527
+ >>> s.eval("path.__file__")
528
+ simpleeval.FeatureNotAvailable: Access to private attribute '__file__'...
529
+
530
+ If you really really need that - you can make your own wrappers and overrides.
531
+ But I advise against it.
485
532
 
486
533
  Other...
487
534
  --------
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "simpleeval"
3
- version = "1.0.4"
3
+ version = "1.0.5"
4
4
  requires-python = ">=3.9"
5
5
  readme = "README.rst"
6
6
  description = "A simple, safe single expression evaluator library."
@@ -63,6 +63,7 @@ Contributors:
63
63
  - cedk (Cédric Krier) <ced@b2ck.com> Allow running tests with Werror
64
64
  - decorator-factory <decorator-factory@protonmail.com> More security fixes
65
65
  - lkruitwagen (Lucas Kruitwagen) Adding support for dict comprehensions
66
+ - ByamB4 (Byambadalai) Reported breakout via module & disallowed functions as object attrs
66
67
 
67
68
  -------------------------------------
68
69
  Basic Usage:
@@ -104,10 +105,12 @@ well:
104
105
 
105
106
  import ast
106
107
  import operator as op
108
+ import os
107
109
  import sys
110
+ import types
108
111
  import warnings
109
112
  from random import random
110
- from typing import Type, Dict, Set, Union
113
+ from typing import Type, Dict, Set, Union, Hashable
111
114
 
112
115
  ########################################
113
116
  # Module wide 'globals'
@@ -136,7 +139,21 @@ DISALLOW_METHODS = [
136
139
  # their functionality is required, then please wrap them up in a safe container. And think
137
140
  # very hard about it first. And don't say I didn't warn you.
138
141
  # builtins is a dict in python >3.6 but a module before
139
- DISALLOW_FUNCTIONS = {type, isinstance, eval, getattr, setattr, repr, compile, open, exec}
142
+ DISALLOW_FUNCTIONS = {
143
+ type,
144
+ isinstance,
145
+ eval,
146
+ getattr,
147
+ setattr,
148
+ repr,
149
+ compile,
150
+ open,
151
+ exec,
152
+ globals,
153
+ locals,
154
+ os.popen,
155
+ os.system,
156
+ }
140
157
  if hasattr(__builtins__, "help") or (
141
158
  hasattr(__builtins__, "__contains__") and "help" in __builtins__ # type: ignore
142
159
  ):
@@ -369,6 +386,54 @@ class MultipleExpressions(UserWarning):
369
386
  pass
370
387
 
371
388
 
389
+ # Sentinal used during attr access
390
+ _ATTR_NOT_FOUND = object()
391
+
392
+
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
+
372
437
  ########################################
373
438
  # Default simple functions to include:
374
439
 
@@ -546,6 +611,28 @@ class SimpleEval(object): # pylint: disable=too-few-public-methods
546
611
  def __del__(self):
547
612
  self.nodes = None
548
613
 
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
+
549
636
  @staticmethod
550
637
  def parse(expr):
551
638
  """parse an expression into a node tree"""
@@ -580,7 +667,9 @@ class SimpleEval(object): # pylint: disable=too-few-public-methods
580
667
  "Sorry, {0} is not available in this evaluator".format(type(node).__name__)
581
668
  )
582
669
 
583
- return handler(node)
670
+ result = handler(node)
671
+ self._check_disallowed_items(result)
672
+ return result
584
673
 
585
674
  def _eval_expr(self, node):
586
675
  return self._eval(node.value)
@@ -759,18 +848,25 @@ class SimpleEval(object): # pylint: disable=too-few-public-methods
759
848
  f"Sorry, '.{node.attr}' access not allowed on '{type_to_check}'"
760
849
  )
761
850
 
851
+ item = _ATTR_NOT_FOUND
852
+
762
853
  # Maybe the base object is an actual object, not just a dict
763
854
  try:
764
- return getattr(node_evaluated, node.attr)
855
+ item = getattr(node_evaluated, node.attr)
765
856
  except (AttributeError, TypeError):
766
- pass
767
-
768
- # TODO: is this a good idea? Try and look for [x] if .x doesn't work?
769
- if self.ATTR_INDEX_FALLBACK:
770
- try:
771
- return node_evaluated[node.attr]
772
- except (KeyError, TypeError):
773
- pass
857
+ # TODO: is this a good idea? Try and look for [x] if .x doesn't work?
858
+ if self.ATTR_INDEX_FALLBACK:
859
+ try:
860
+ item = node_evaluated[node.attr]
861
+ except (KeyError, TypeError):
862
+ pass
863
+
864
+ if item is not _ATTR_NOT_FOUND:
865
+ if isinstance(item, types.ModuleType):
866
+ raise FeatureNotAvailable("Sorry, modules are not allowed in attribute access")
867
+ if isinstance(item, Hashable) and item in DISALLOW_FUNCTIONS:
868
+ raise FeatureNotAvailable("This function is forbidden")
869
+ return item
774
870
 
775
871
  # If it is neither, raise an exception
776
872
  raise AttributeDoesNotExist(node.attr, self.expr)
@@ -25,6 +25,7 @@ from simpleeval import (
25
25
  FeatureNotAvailable,
26
26
  FunctionNotDefined,
27
27
  InvalidExpression,
28
+ ModuleWrapper,
28
29
  NameNotDefined,
29
30
  OperatorNotDefined,
30
31
  SimpleEval,
@@ -590,6 +591,527 @@ class TestTryingToBreakOut(DRYTest):
590
591
 
591
592
  simpleeval.DISALLOW_PREFIXES = dis
592
593
 
594
+ def test_breakout_via_module_access(self):
595
+ import os.path
596
+
597
+ s = SimpleEval(names={"path": os.path})
598
+
599
+ with self.assertRaises(FeatureNotAvailable):
600
+ s.eval("path.os.popen('id').read()")
601
+
602
+ def test_breakout_via_module_access_attr(self):
603
+ import os.path
604
+
605
+ class Foo:
606
+ p = os.path
607
+
608
+ s = SimpleEval(names={"thing": Foo()})
609
+
610
+ with self.assertRaises(FeatureNotAvailable):
611
+ s.eval("thing.p.os.popen('id').read()")
612
+
613
+ def test_breakout_via_disallowed_functions_as_attrs(self):
614
+ class Foo:
615
+ p = exec
616
+
617
+ s = SimpleEval(names={"thing": Foo()})
618
+
619
+ with self.assertRaises(FeatureNotAvailable):
620
+ s.eval("thing.p('exit')")
621
+
622
+ def test_breakout_forbidden_function_in_list(self):
623
+ """Disallowed functions in lists should be blocked"""
624
+ s = SimpleEval(names={"funcs": [exec, eval]})
625
+
626
+ with self.assertRaises(FeatureNotAvailable):
627
+ s.eval("funcs[0]('exit')")
628
+
629
+ with self.assertRaises(FeatureNotAvailable):
630
+ s.eval("funcs[1]('1+1')")
631
+
632
+ def test_breakout_module_in_list(self):
633
+ """Modules in lists should be blocked"""
634
+ import os.path
635
+
636
+ s = SimpleEval(names={"things": [os.path, os.system]})
637
+
638
+ with self.assertRaises(FeatureNotAvailable):
639
+ s.eval("things[0].os.popen('id').read()")
640
+
641
+ def test_breakout_forbidden_function_in_dict_value(self):
642
+ """Disallowed functions as dict values should be blocked"""
643
+ s = SimpleEval(names={"funcs": {"bad": exec, "evil": eval}})
644
+
645
+ with self.assertRaises(FeatureNotAvailable):
646
+ s.eval("funcs['bad']('exit')")
647
+
648
+ with self.assertRaises(FeatureNotAvailable):
649
+ s.eval("funcs['evil']('1+1')")
650
+
651
+ def test_breakout_module_in_dict_value(self):
652
+ """Modules as dict values should be blocked"""
653
+ import os.path
654
+
655
+ s = SimpleEval(names={"things": {"p": os.path, "s": os.system}})
656
+
657
+ with self.assertRaises(FeatureNotAvailable):
658
+ s.eval("things['p'].os.popen('id').read()")
659
+
660
+ def test_breakout_function_returning_forbidden_function(self):
661
+ """Functions returning disallowed functions should be blocked"""
662
+
663
+ def get_evil():
664
+ return exec
665
+
666
+ s = SimpleEval(names={}, functions={"get_evil": get_evil})
667
+
668
+ with self.assertRaises(FeatureNotAvailable):
669
+ s.eval("get_evil()('exit')")
670
+
671
+ def test_breakout_function_returning_module(self):
672
+ """Functions returning modules should be blocked"""
673
+ import os.path
674
+
675
+ def get_module():
676
+ return os.path
677
+
678
+ s = SimpleEval(names={}, functions={"get_module": get_module})
679
+
680
+ with self.assertRaises(FeatureNotAvailable):
681
+ s.eval("get_module().os.popen('id').read()")
682
+
683
+ def test_dunder_all_in_module(self):
684
+ """__all__ should be blocked (starts with _)"""
685
+ import os
686
+
687
+ s = SimpleEval(names={"os": os})
688
+
689
+ with self.assertRaises(FeatureNotAvailable):
690
+ s.eval("os.__all__")
691
+
692
+ def test_dunder_dict_in_module(self):
693
+ """__dict__ should be blocked (starts with _)"""
694
+ import os
695
+
696
+ s = SimpleEval(names={"os": os})
697
+
698
+ with self.assertRaises(FeatureNotAvailable):
699
+ s.eval("os.__dict__")
700
+
701
+ def test_forbidden_method_in_tuple(self):
702
+ """Disallowed functions in tuples should be blocked"""
703
+ s = SimpleEval(names={"funcs": (exec, eval)})
704
+
705
+ with self.assertRaises(FeatureNotAvailable):
706
+ s.eval("funcs[0]('exit')")
707
+
708
+ def test_module_in_tuple(self):
709
+ """Modules in tuples should be blocked"""
710
+ import os
711
+
712
+ s = SimpleEval(names={"mods": (os.path, os.system)})
713
+
714
+ with self.assertRaises(FeatureNotAvailable):
715
+ s.eval("mods[0].os.popen('id').read()")
716
+
717
+ def test_breakout_via_nested_container_forbidden_func(self):
718
+ """Disallowed functions nested in containers should be blocked"""
719
+ s = SimpleEval(names={"data": {"nested": {"funcs": [exec]}}})
720
+
721
+ with self.assertRaises(FeatureNotAvailable):
722
+ s.eval("data['nested']['funcs'][0]('exit')")
723
+
724
+ def test_breakout_via_nested_container_module(self):
725
+ """Modules nested in containers should be blocked"""
726
+ import os
727
+
728
+ s = SimpleEval(names={"data": {"mods": {"p": os.path}}})
729
+
730
+ with self.assertRaises(FeatureNotAvailable):
731
+ s.eval("data['mods']['p'].os.popen('id').read()")
732
+
733
+ def test_forbidden_methods_on_allowed_attrs(self):
734
+ """Disallowed methods listed in DISALLOW_METHODS should be
735
+ blocked"""
736
+ s = SimpleEval()
737
+
738
+ # format and format_map are in DISALLOW_METHODS
739
+ with self.assertRaises(FeatureNotAvailable):
740
+ s.eval("'test {0}'.format")
741
+
742
+ with self.assertRaises(FeatureNotAvailable):
743
+ s.eval("'test'.format_map({0: 'x'})")
744
+
745
+ # __mro__ is in DISALLOW_METHODS
746
+ with self.assertRaises(FeatureNotAvailable):
747
+ s.eval("'test'.mro")
748
+
749
+ def test_function_returning_forbidden_method(self):
750
+ """Functions returning disallowed methods should be blocked"""
751
+
752
+ def get_exec_module():
753
+ import os
754
+
755
+ return os
756
+
757
+ s = SimpleEval(names={}, functions={"get_os": get_exec_module})
758
+
759
+ with self.assertRaises(FeatureNotAvailable):
760
+ s.eval("get_os().__name__")
761
+
762
+ def test_compound_module_submodule_access(self):
763
+ """Accessing submodules of a passed module should be blocked"""
764
+ import os.path
765
+
766
+ s = SimpleEval(names={"path": os.path})
767
+
768
+ with self.assertRaises(FeatureNotAvailable):
769
+ s.eval("path.os")
770
+
771
+ def test_forbidden_func_via_class_method(self):
772
+ """Accessing forbidden functions via class methods should be
773
+ blocked"""
774
+
775
+ class Container:
776
+ @staticmethod
777
+ def get_exec():
778
+ return exec
779
+
780
+ s = SimpleEval(names={"c": Container()})
781
+
782
+ with self.assertRaises(FeatureNotAvailable):
783
+ s.eval("c.get_exec()('exit')")
784
+
785
+ def test_module_via_class_method(self):
786
+ """Accessing modules via class methods should be blocked"""
787
+ import os
788
+
789
+ class Container:
790
+ @staticmethod
791
+ def get_os():
792
+ return os
793
+
794
+ s = SimpleEval(names={"c": Container()})
795
+
796
+ with self.assertRaises(FeatureNotAvailable):
797
+ s.eval("c.get_os().popen('id').read()")
798
+
799
+ def test_forbidden_func_via_property(self):
800
+ """Accessing forbidden functions via properties should be
801
+ blocked"""
802
+
803
+ class Container:
804
+ @property
805
+ def evil(self):
806
+ return exec
807
+
808
+ s = SimpleEval(names={"c": Container()})
809
+
810
+ with self.assertRaises(FeatureNotAvailable):
811
+ s.eval("c.evil('exit')")
812
+
813
+ def test_module_via_property(self):
814
+ """Accessing modules via properties should be blocked"""
815
+ import os
816
+
817
+ class Container:
818
+ @property
819
+ def mod(self):
820
+ return os
821
+
822
+ s = SimpleEval(names={"c": Container()})
823
+
824
+ with self.assertRaises(FeatureNotAvailable):
825
+ s.eval("c.mod.popen('id').read()")
826
+
827
+ def test_forbidden_function_direct_from_names(self):
828
+ """Forbidden functions passed directly in names should
829
+ be blocked when accessed"""
830
+ s = SimpleEval(names={"evil": exec})
831
+
832
+ with self.assertRaises(FeatureNotAvailable):
833
+ s.eval("evil")
834
+
835
+ def test_module_direct_from_names(self):
836
+ """Modules passed directly in names should be blocked
837
+ when accessed"""
838
+ import os
839
+
840
+ s = SimpleEval(names={"m": os})
841
+
842
+ with self.assertRaises(FeatureNotAvailable):
843
+ s.eval("m")
844
+
845
+ def test_forbidden_function_via_callable_name_handler(self):
846
+ """Forbidden functions from callable name handlers should
847
+ be blocked"""
848
+
849
+ def name_handler(node):
850
+ if node.id == "evil":
851
+ return exec
852
+ raise simpleeval.NameNotDefined(node.id, "")
853
+
854
+ s = SimpleEval(names=name_handler)
855
+
856
+ with self.assertRaises(FeatureNotAvailable):
857
+ s.eval("evil")
858
+
859
+ def test_module_via_callable_name_handler(self):
860
+ """Modules from callable name handlers should be blocked"""
861
+ import os
862
+
863
+ def name_handler(node):
864
+ if node.id == "m":
865
+ return os
866
+ raise simpleeval.NameNotDefined(node.id, "")
867
+
868
+ s = SimpleEval(names=name_handler)
869
+
870
+ with self.assertRaises(FeatureNotAvailable):
871
+ s.eval("m")
872
+
873
+ def test_forbidden_function_passed_to_custom_function(self):
874
+ """Passing forbidden functions to custom functions should be
875
+ blocked - they can be executed by the custom function"""
876
+
877
+ def evil_caller(func):
878
+ return func("print('pwned')")
879
+
880
+ s = SimpleEval(names={"evil": exec}, functions={"evil_caller": evil_caller})
881
+
882
+ with self.assertRaises(FeatureNotAvailable):
883
+ s.eval("evil_caller(evil)")
884
+
885
+ def test_module_passed_to_custom_function(self):
886
+ """Passing modules to custom functions should be blocked - they
887
+ can be used by the custom function"""
888
+ import os
889
+
890
+ def os_caller(mod):
891
+ return mod.system("id")
892
+
893
+ s = SimpleEval(names={"m": os}, functions={"os_caller": os_caller})
894
+
895
+ with self.assertRaises(FeatureNotAvailable):
896
+ s.eval("os_caller(m)")
897
+
898
+ def test_forbidden_function_in_list_passed_to_custom_function(self):
899
+ """Forbidden functions in containers passed to custom functions
900
+ should be blocked"""
901
+
902
+ def extract_and_call(items):
903
+ return items[0]("print('pwned')")
904
+
905
+ s = SimpleEval(names={"funcs": [exec, eval]}, functions={"extract": extract_and_call})
906
+
907
+ with self.assertRaises(FeatureNotAvailable):
908
+ s.eval("extract(funcs)")
909
+
910
+ def test_module_in_list_passed_to_custom_function(self):
911
+ """Modules in containers passed to custom functions should be
912
+ blocked"""
913
+ import os
914
+
915
+ def extract_and_use(items):
916
+ return items[0].system("id")
917
+
918
+ s = SimpleEval(names={"mods": [os.path, os]}, functions={"extract": extract_and_use})
919
+
920
+ with self.assertRaises(FeatureNotAvailable):
921
+ s.eval("extract(mods)")
922
+
923
+ def test_forbidden_function_in_dict_passed_to_custom_function(self):
924
+ """Forbidden functions in dicts passed to custom functions should
925
+ be blocked"""
926
+
927
+ def extract_and_call(d):
928
+ return d["bad"]("print('pwned')")
929
+
930
+ s = SimpleEval(
931
+ names={"funcs": {"bad": exec, "good": print}}, functions={"extract": extract_and_call}
932
+ )
933
+
934
+ with self.assertRaises(FeatureNotAvailable):
935
+ s.eval("extract(funcs)")
936
+
937
+ def test_module_in_dict_passed_to_custom_function(self):
938
+ """Modules in dicts passed to custom functions should be blocked"""
939
+ import os
940
+
941
+ def extract_and_use(d):
942
+ return d["m"].system("id")
943
+
944
+ s = SimpleEval(
945
+ names={"mods": {"m": os, "p": os.path}}, functions={"extract": extract_and_use}
946
+ )
947
+
948
+ with self.assertRaises(FeatureNotAvailable):
949
+ s.eval("extract(mods)")
950
+
951
+
952
+ class TestModuleWrapper(unittest.TestCase):
953
+ """Test the ModuleWrapper class itself"""
954
+
955
+ def test_module_wrapper_requires_module(self):
956
+ """ModuleWrapper should reject non-module types"""
957
+ with self.assertRaises(TypeError):
958
+ ModuleWrapper("not a module")
959
+
960
+ with self.assertRaises(TypeError):
961
+ ModuleWrapper(42)
962
+
963
+ with self.assertRaises(TypeError):
964
+ ModuleWrapper({})
965
+
966
+ def test_module_wrapper_allows_valid_module(self):
967
+ """ModuleWrapper should accept valid modules"""
968
+ import os.path
969
+
970
+ wrapper = ModuleWrapper(os.path)
971
+ self.assertIsNotNone(wrapper)
972
+
973
+ def test_module_wrapper_blocks_private_attrs(self):
974
+ """ModuleWrapper should block access to private attributes"""
975
+ import os.path
976
+
977
+ wrapper = ModuleWrapper(os.path)
978
+
979
+ with self.assertRaises(FeatureNotAvailable):
980
+ wrapper.__all__
981
+
982
+ with self.assertRaises(FeatureNotAvailable):
983
+ wrapper._internal
984
+
985
+ def test_module_wrapper_allows_public_attrs(self):
986
+ """ModuleWrapper should allow access to public attributes"""
987
+ import os.path
988
+
989
+ wrapper = ModuleWrapper(os.path)
990
+ # Should not raise
991
+ _ = wrapper.exists
992
+
993
+ def test_module_wrapper_blocks_disallowed_methods(self):
994
+ """ModuleWrapper should block access to methods in DISALLOW_METHODS"""
995
+ import os
996
+
997
+ wrapper = ModuleWrapper(os)
998
+
999
+ with self.assertRaises(FeatureNotAvailable):
1000
+ wrapper.mro
1001
+
1002
+ def test_module_wrapper_with_allowed_attrs_allows_whitelisted(self):
1003
+ """ModuleWrapper with allowed_attrs should allow whitelisted
1004
+ attributes"""
1005
+ import os.path
1006
+
1007
+ wrapper = ModuleWrapper(os.path, allowed_attrs={"exists", "join"})
1008
+
1009
+ # Should not raise
1010
+ _ = wrapper.exists
1011
+ _ = wrapper.join
1012
+
1013
+ def test_module_wrapper_with_allowed_attrs_blocks_non_whitelisted(self):
1014
+ """ModuleWrapper with allowed_attrs should block non-whitelisted
1015
+ attributes"""
1016
+ import os.path
1017
+
1018
+ wrapper = ModuleWrapper(os.path, allowed_attrs={"exists"})
1019
+
1020
+ with self.assertRaises(FeatureNotAvailable):
1021
+ wrapper.join
1022
+
1023
+ def test_module_wrapper_getattr_returns_actual_attribute(self):
1024
+ """ModuleWrapper.__getattr__ should return the actual module
1025
+ attribute"""
1026
+ import os.path
1027
+
1028
+ wrapper = ModuleWrapper(os.path)
1029
+ result = wrapper.exists
1030
+
1031
+ # Should be the actual function
1032
+ self.assertEqual(result, os.path.exists)
1033
+
1034
+
1035
+ class TestModuleWrapperAccess(DRYTest):
1036
+ """Test ModuleWrapper integration with SimpleEval"""
1037
+
1038
+ def test_unwrapped_module_blocked(self):
1039
+ """Unwrapped modules in names should be blocked"""
1040
+ import os.path
1041
+
1042
+ s = SimpleEval(names={"path": os.path})
1043
+
1044
+ with self.assertRaises(FeatureNotAvailable):
1045
+ s.eval("path")
1046
+
1047
+ def test_wrapped_module_allowed(self):
1048
+ """ModuleWrapper should allow module access in eval"""
1049
+ import os.path
1050
+
1051
+ s = SimpleEval(names={"path": ModuleWrapper(os.path)})
1052
+
1053
+ result = s.eval("path.exists('/etc/passwd')")
1054
+ self.assertTrue(isinstance(result, bool))
1055
+
1056
+ def test_wrapped_module_private_attrs_blocked(self):
1057
+ """ModuleWrapper should block private attrs in eval"""
1058
+ import os.path
1059
+
1060
+ s = SimpleEval(names={"path": ModuleWrapper(os.path)})
1061
+
1062
+ with self.assertRaises(FeatureNotAvailable):
1063
+ s.eval("path.__all__")
1064
+
1065
+ def test_wrapped_module_with_whitelist(self):
1066
+ """ModuleWrapper with whitelist should allow whitelisted attrs"""
1067
+ import os.path
1068
+
1069
+ s = SimpleEval(names={"path": ModuleWrapper(os.path, allowed_attrs={"exists"})})
1070
+
1071
+ result = s.eval("path.exists('/etc/passwd')")
1072
+ self.assertTrue(isinstance(result, bool))
1073
+
1074
+ def test_wrapped_module_with_whitelist_blocks_others(self):
1075
+ """ModuleWrapper with whitelist should block non-whitelisted
1076
+ attrs"""
1077
+ import os.path
1078
+
1079
+ s = SimpleEval(names={"path": ModuleWrapper(os.path, allowed_attrs={"exists"})})
1080
+
1081
+ with self.assertRaises(FeatureNotAvailable):
1082
+ s.eval("path.join('a', 'b')")
1083
+
1084
+ def test_wrapped_module_passed_to_function(self):
1085
+ """ModuleWrapper can be passed to custom functions"""
1086
+
1087
+ def process_path(path_mod):
1088
+ return path_mod.exists("/etc/passwd")
1089
+
1090
+ import os.path
1091
+
1092
+ s = SimpleEval(names={"path": ModuleWrapper(os.path)}, functions={"process": process_path})
1093
+
1094
+ result = s.eval("process(path)")
1095
+ self.assertTrue(isinstance(result, bool))
1096
+
1097
+ def test_wrapped_module_in_container(self):
1098
+ """ModuleWrapper can be stored in containers"""
1099
+ import os.path
1100
+
1101
+ s = SimpleEval(names={"items": [ModuleWrapper(os.path), 1, 2]})
1102
+
1103
+ result = s.eval("items")
1104
+ self.assertEqual(len(result), 3)
1105
+
1106
+ def test_wrapped_module_in_dict_container(self):
1107
+ """ModuleWrapper can be stored in dicts"""
1108
+ import os.path
1109
+
1110
+ s = SimpleEval(names={"data": {"path": ModuleWrapper(os.path), "value": 42}})
1111
+
1112
+ result = s.eval("data['value']")
1113
+ self.assertEqual(result, 42)
1114
+
593
1115
 
594
1116
  class TestCompoundTypes(DRYTest):
595
1117
  """Test the compound-types edition of the library"""
@@ -1250,33 +1772,37 @@ class TestShortCircuiting(DRYTest):
1250
1772
 
1251
1773
 
1252
1774
  class TestDisallowedFunctions(DRYTest):
1253
- def test_functions_are_disallowed_at_init(self):
1254
- DISALLOWED = [type, isinstance, eval, getattr, setattr, help, repr, compile, open, exec]
1255
-
1256
- for f in simpleeval.DISALLOW_FUNCTIONS:
1257
- assert f in DISALLOWED
1775
+ def test_functions_in_disallowed_functions_list(self):
1776
+ # a bit of double-entry testing. probably pointless.
1777
+ assert simpleeval.DISALLOW_FUNCTIONS.issuperset(
1778
+ {
1779
+ type,
1780
+ isinstance,
1781
+ eval,
1782
+ getattr,
1783
+ setattr,
1784
+ help,
1785
+ repr,
1786
+ compile,
1787
+ open,
1788
+ exec,
1789
+ os.popen,
1790
+ os.system,
1791
+ }
1792
+ )
1258
1793
 
1259
- for x in DISALLOWED:
1794
+ def test_functions_are_disallowed_at_init(self):
1795
+ for dangerous_function in simpleeval.DISALLOW_FUNCTIONS:
1260
1796
  with self.assertRaises(FeatureNotAvailable):
1261
- SimpleEval(functions={"foo": x})
1797
+ SimpleEval(functions={"foo": dangerous_function})
1262
1798
 
1263
1799
  def test_functions_are_disallowed_in_expressions(self):
1264
- DISALLOWED = [type, isinstance, eval, getattr, setattr, help, repr, compile, open, exec]
1265
-
1266
- for f in simpleeval.DISALLOW_FUNCTIONS:
1267
- assert f in DISALLOWED
1268
-
1269
- DF = simpleeval.DEFAULT_FUNCTIONS.copy()
1270
-
1271
- for x in DISALLOWED:
1272
- simpleeval.DEFAULT_FUNCTIONS = DF.copy()
1800
+ for dangerous_function in simpleeval.DISALLOW_FUNCTIONS:
1273
1801
  with self.assertRaises(FeatureNotAvailable):
1274
1802
  s = SimpleEval()
1275
- s.functions["foo"] = x
1803
+ s.functions["foo"] = dangerous_function
1276
1804
  s.eval("foo(42)")
1277
1805
 
1278
- simpleeval.DEFAULT_FUNCTIONS = DF.copy()
1279
-
1280
1806
  def test_breakout_via_generator(self):
1281
1807
  # Thanks decorator-factory
1282
1808
  class Foo:
File without changes
File without changes