simpleeval 1.0.4__tar.gz → 1.0.6__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.6
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.6"
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,7 +105,9 @@ 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
113
  from typing import Type, Dict, Set, Union
@@ -129,6 +132,17 @@ DISALLOW_METHODS = [
129
132
  "exec",
130
133
  ]
131
134
 
135
+ ########################################
136
+ # Tiny helpers:
137
+
138
+
139
+ def is_hashable(value):
140
+ try:
141
+ return hash(value)
142
+ except TypeError:
143
+ return False
144
+
145
+
132
146
  # Disallow functions:
133
147
  # This, strictly speaking, is not necessary. These /should/ never be accessible anyway,
134
148
  # if DISALLOW_PREFIXES and DISALLOW_METHODS are all right. This is here to try and help
@@ -136,7 +150,21 @@ DISALLOW_METHODS = [
136
150
  # their functionality is required, then please wrap them up in a safe container. And think
137
151
  # very hard about it first. And don't say I didn't warn you.
138
152
  # builtins is a dict in python >3.6 but a module before
139
- DISALLOW_FUNCTIONS = {type, isinstance, eval, getattr, setattr, repr, compile, open, exec}
153
+ DISALLOW_FUNCTIONS = {
154
+ type,
155
+ isinstance,
156
+ eval,
157
+ getattr,
158
+ setattr,
159
+ repr,
160
+ compile,
161
+ open,
162
+ exec,
163
+ globals,
164
+ locals,
165
+ os.popen,
166
+ os.system,
167
+ }
140
168
  if hasattr(__builtins__, "help") or (
141
169
  hasattr(__builtins__, "__contains__") and "help" in __builtins__ # type: ignore
142
170
  ):
@@ -369,6 +397,54 @@ class MultipleExpressions(UserWarning):
369
397
  pass
370
398
 
371
399
 
400
+ # Sentinal used during attr access
401
+ _ATTR_NOT_FOUND = object()
402
+
403
+
404
+ class ModuleWrapper:
405
+ """Wraps a module to safely expose it in expressions.
406
+
407
+ By default, modules are not allowed in simpleeval names to prevent
408
+ accidental or malicious access to dangerous functions. ModuleWrapper
409
+ allows explicit opt-in to module access while still enforcing
410
+ restrictions on dangerous methods and functions.
411
+
412
+ Example:
413
+ >>> from simpleeval import SimpleEval, ModuleWrapper
414
+ >>> import os.path
415
+ >>> s = SimpleEval(names={'path': ModuleWrapper(os.path)})
416
+ >>> s.eval('path.exists("/etc/passwd")') # Works
417
+ """
418
+
419
+ def __init__(self, module, allowed_attrs=None):
420
+ """
421
+ Args:
422
+ module: The module to wrap
423
+ allowed_attrs: Optional set of allowed attribute names.
424
+ If None, all public attributes are allowed
425
+ (but still subject to DISALLOW_METHODS checks).
426
+ """
427
+ if not isinstance(module, types.ModuleType):
428
+ raise TypeError(f"ModuleWrapper requires a module, got {type(module)}")
429
+ self._module = module
430
+ self._allowed_attrs = allowed_attrs
431
+
432
+ def __getattr__(self, name):
433
+ # Block private/magic attributes
434
+ if name.startswith("_"):
435
+ raise FeatureNotAvailable(f"Access to private attribute '{name}' is not allowed")
436
+
437
+ # Check if attribute is in disallowed methods list
438
+ if name in DISALLOW_METHODS:
439
+ raise FeatureNotAvailable(f"Method '{name}' is not allowed on modules")
440
+
441
+ # Check allowed_attrs whitelist if specified
442
+ if self._allowed_attrs is not None and name not in self._allowed_attrs:
443
+ raise FeatureNotAvailable(f"Access to '{name}' is not allowed on this wrapped module")
444
+
445
+ return getattr(self._module, name)
446
+
447
+
372
448
  ########################################
373
449
  # Default simple functions to include:
374
450
 
@@ -546,6 +622,28 @@ class SimpleEval(object): # pylint: disable=too-few-public-methods
546
622
  def __del__(self):
547
623
  self.nodes = None
548
624
 
625
+ def _check_disallowed_items(self, item):
626
+ """Check if item contains disallowed functions or modules.
627
+ Recursively checks containers (list, dict, tuple).
628
+ Raises FeatureNotAvailable if forbidden content found.
629
+ ModuleWrapper instances are allowed (explicit opt-in to module access).
630
+ """
631
+ # Allow ModuleWrapper (explicit opt-in to module access)
632
+ if isinstance(item, ModuleWrapper):
633
+ return
634
+
635
+ if isinstance(item, types.ModuleType):
636
+ raise FeatureNotAvailable("Sorry, modules are not allowed")
637
+ if is_hashable(item) and item in DISALLOW_FUNCTIONS:
638
+ raise FeatureNotAvailable("This function is forbidden")
639
+
640
+ if isinstance(item, (list, tuple)):
641
+ for element in item:
642
+ self._check_disallowed_items(element)
643
+ elif isinstance(item, dict):
644
+ for value in item.values():
645
+ self._check_disallowed_items(value)
646
+
549
647
  @staticmethod
550
648
  def parse(expr):
551
649
  """parse an expression into a node tree"""
@@ -580,7 +678,9 @@ class SimpleEval(object): # pylint: disable=too-few-public-methods
580
678
  "Sorry, {0} is not available in this evaluator".format(type(node).__name__)
581
679
  )
582
680
 
583
- return handler(node)
681
+ result = handler(node)
682
+ self._check_disallowed_items(result)
683
+ return result
584
684
 
585
685
  def _eval_expr(self, node):
586
686
  return self._eval(node.value)
@@ -759,18 +859,25 @@ class SimpleEval(object): # pylint: disable=too-few-public-methods
759
859
  f"Sorry, '.{node.attr}' access not allowed on '{type_to_check}'"
760
860
  )
761
861
 
862
+ item = _ATTR_NOT_FOUND
863
+
762
864
  # Maybe the base object is an actual object, not just a dict
763
865
  try:
764
- return getattr(node_evaluated, node.attr)
866
+ item = getattr(node_evaluated, node.attr)
765
867
  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
868
+ # TODO: is this a good idea? Try and look for [x] if .x doesn't work?
869
+ if self.ATTR_INDEX_FALLBACK:
870
+ try:
871
+ item = node_evaluated[node.attr]
872
+ except (KeyError, TypeError):
873
+ pass
874
+
875
+ if item is not _ATTR_NOT_FOUND:
876
+ if isinstance(item, types.ModuleType):
877
+ raise FeatureNotAvailable("Sorry, modules are not allowed in attribute access")
878
+ if is_hashable(item) and item in DISALLOW_FUNCTIONS:
879
+ raise FeatureNotAvailable("This function is forbidden")
880
+ return item
774
881
 
775
882
  # If it is neither, raise an exception
776
883
  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,
@@ -348,6 +349,15 @@ class TestFunctions(DRYTest):
348
349
  self.t("foo(mult=2, to_return=4)", 8)
349
350
  self.t("foo(2, 10)", 20)
350
351
 
352
+ def test_function_with_list_args(self):
353
+ # Regression test, makes sure we can pass lists (non-hashable) items as
354
+ # kwargs to functions.
355
+
356
+ def func(*args, **kwargs):
357
+ return 42
358
+
359
+ simple_eval("test(boo=x)", functions={"test": func}, names={"x": [1, 2]})
360
+
351
361
 
352
362
  class TestOperators(DRYTest):
353
363
  """Test adding in new operators, removing them, make sure it works."""
@@ -590,6 +600,525 @@ class TestTryingToBreakOut(DRYTest):
590
600
 
591
601
  simpleeval.DISALLOW_PREFIXES = dis
592
602
 
603
+ def test_breakout_via_module_access(self):
604
+ import os.path
605
+
606
+ s = SimpleEval(names={"path": os.path})
607
+
608
+ with self.assertRaises(FeatureNotAvailable):
609
+ s.eval("path.os.popen('id').read()")
610
+
611
+ def test_breakout_via_module_access_attr(self):
612
+ import os.path
613
+
614
+ class Foo:
615
+ p = os.path
616
+
617
+ s = SimpleEval(names={"thing": Foo()})
618
+
619
+ with self.assertRaises(FeatureNotAvailable):
620
+ s.eval("thing.p.os.popen('id').read()")
621
+
622
+ def test_breakout_via_disallowed_functions_as_attrs(self):
623
+ class Foo:
624
+ p = exec
625
+
626
+ s = SimpleEval(names={"thing": Foo()})
627
+
628
+ with self.assertRaises(FeatureNotAvailable):
629
+ s.eval("thing.p('exit')")
630
+
631
+ def test_breakout_forbidden_function_in_list(self):
632
+ """Disallowed functions in lists should be blocked"""
633
+ s = SimpleEval(names={"funcs": [exec, eval]})
634
+
635
+ with self.assertRaises(FeatureNotAvailable):
636
+ s.eval("funcs[0]('exit')")
637
+
638
+ with self.assertRaises(FeatureNotAvailable):
639
+ s.eval("funcs[1]('1+1')")
640
+
641
+ def test_breakout_module_in_list(self):
642
+ """Modules in lists should be blocked"""
643
+ import os.path
644
+
645
+ s = SimpleEval(names={"things": [os.path, os.system]})
646
+
647
+ with self.assertRaises(FeatureNotAvailable):
648
+ s.eval("things[0].os.popen('id').read()")
649
+
650
+ def test_breakout_forbidden_function_in_dict_value(self):
651
+ """Disallowed functions as dict values should be blocked"""
652
+ s = SimpleEval(names={"funcs": {"bad": exec, "evil": eval}})
653
+
654
+ with self.assertRaises(FeatureNotAvailable):
655
+ s.eval("funcs['bad']('exit')")
656
+
657
+ with self.assertRaises(FeatureNotAvailable):
658
+ s.eval("funcs['evil']('1+1')")
659
+
660
+ def test_breakout_module_in_dict_value(self):
661
+ """Modules as dict values should be blocked"""
662
+ import os.path
663
+
664
+ s = SimpleEval(names={"things": {"p": os.path, "s": os.system}})
665
+
666
+ with self.assertRaises(FeatureNotAvailable):
667
+ s.eval("things['p'].os.popen('id').read()")
668
+
669
+ def test_breakout_function_returning_forbidden_function(self):
670
+ """Functions returning disallowed functions should be blocked"""
671
+
672
+ def get_evil():
673
+ return exec # pragma: no cover
674
+
675
+ s = SimpleEval(names={}, functions={"get_evil": get_evil})
676
+
677
+ with self.assertRaises(FeatureNotAvailable):
678
+ s.eval("get_evil()('exit')")
679
+
680
+ def test_breakout_function_returning_module(self):
681
+ """Functions returning modules should be blocked"""
682
+ import os.path
683
+
684
+ def get_module():
685
+ return os.path
686
+
687
+ s = SimpleEval(names={}, functions={"get_module": get_module})
688
+
689
+ with self.assertRaises(FeatureNotAvailable):
690
+ s.eval("get_module().os.popen('id').read()")
691
+
692
+ def test_dunder_all_in_module(self):
693
+ """__all__ 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.__all__")
700
+
701
+ def test_dunder_dict_in_module(self):
702
+ """__dict__ should be blocked (starts with _)"""
703
+ import os
704
+
705
+ s = SimpleEval(names={"os": os})
706
+
707
+ with self.assertRaises(FeatureNotAvailable):
708
+ s.eval("os.__dict__")
709
+
710
+ def test_forbidden_method_in_tuple(self):
711
+ """Disallowed functions in tuples should be blocked"""
712
+ s = SimpleEval(names={"funcs": (exec, eval)})
713
+
714
+ with self.assertRaises(FeatureNotAvailable):
715
+ s.eval("funcs[0]('exit')")
716
+
717
+ def test_module_in_tuple(self):
718
+ """Modules in tuples should be blocked"""
719
+ import os
720
+
721
+ s = SimpleEval(names={"mods": (os.path, os.system)})
722
+
723
+ with self.assertRaises(FeatureNotAvailable):
724
+ s.eval("mods[0].os.popen('id').read()")
725
+
726
+ def test_breakout_via_nested_container_forbidden_func(self):
727
+ """Disallowed functions nested in containers should be blocked"""
728
+ s = SimpleEval(names={"data": {"nested": {"funcs": [exec]}}})
729
+
730
+ with self.assertRaises(FeatureNotAvailable):
731
+ s.eval("data['nested']['funcs'][0]('exit')")
732
+
733
+ def test_breakout_via_nested_container_module(self):
734
+ """Modules nested in containers should be blocked"""
735
+ import os
736
+
737
+ s = SimpleEval(names={"data": {"mods": {"p": os.path}}})
738
+
739
+ with self.assertRaises(FeatureNotAvailable):
740
+ s.eval("data['mods']['p'].os.popen('id').read()")
741
+
742
+ def test_forbidden_methods_on_allowed_attrs(self):
743
+ """Disallowed methods listed in DISALLOW_METHODS should be
744
+ blocked"""
745
+ s = SimpleEval()
746
+
747
+ # format and format_map are in DISALLOW_METHODS
748
+ with self.assertRaises(FeatureNotAvailable):
749
+ s.eval("'test {0}'.format")
750
+
751
+ with self.assertRaises(FeatureNotAvailable):
752
+ s.eval("'test'.format_map({0: 'x'})")
753
+
754
+ # __mro__ is in DISALLOW_METHODS
755
+ with self.assertRaises(FeatureNotAvailable):
756
+ s.eval("'test'.mro")
757
+
758
+ def test_function_returning_forbidden_method(self):
759
+ """Functions returning disallowed methods should be blocked"""
760
+
761
+ def get_exec_module():
762
+ import os # pragma: no cover
763
+
764
+ return os # pragma: no cover
765
+
766
+ s = SimpleEval(names={}, functions={"get_os": get_exec_module})
767
+
768
+ with self.assertRaises(FeatureNotAvailable):
769
+ s.eval("get_os().__name__")
770
+
771
+ def test_compound_module_submodule_access(self):
772
+ """Accessing submodules of a passed module should be blocked"""
773
+ import os.path
774
+
775
+ s = SimpleEval(names={"path": os.path})
776
+
777
+ with self.assertRaises(FeatureNotAvailable):
778
+ s.eval("path.os")
779
+
780
+ def test_forbidden_func_via_class_method(self):
781
+ """Accessing forbidden functions via class methods should be
782
+ blocked"""
783
+
784
+ class Container:
785
+ @staticmethod
786
+ def get_exec():
787
+ return exec # pragma: no cover
788
+
789
+ s = SimpleEval(names={"c": Container()})
790
+
791
+ with self.assertRaises(FeatureNotAvailable):
792
+ s.eval("c.get_exec()('exit')")
793
+
794
+ def test_module_via_class_method(self):
795
+ """Accessing modules via class methods should be blocked"""
796
+ import os
797
+
798
+ class Container:
799
+ @staticmethod
800
+ def get_os():
801
+ return os
802
+
803
+ s = SimpleEval(names={"c": Container()})
804
+
805
+ with self.assertRaises(FeatureNotAvailable):
806
+ s.eval("c.get_os().popen('id').read()")
807
+
808
+ def test_forbidden_func_via_property(self):
809
+ """Accessing forbidden functions via properties should be
810
+ blocked"""
811
+
812
+ class Container:
813
+ @property
814
+ def evil(self):
815
+ return exec
816
+
817
+ s = SimpleEval(names={"c": Container()})
818
+
819
+ with self.assertRaises(FeatureNotAvailable):
820
+ s.eval("c.evil('exit')")
821
+
822
+ def test_module_via_property(self):
823
+ """Accessing modules via properties should be blocked"""
824
+ import os
825
+
826
+ class Container:
827
+ @property
828
+ def mod(self):
829
+ return os
830
+
831
+ s = SimpleEval(names={"c": Container()})
832
+
833
+ with self.assertRaises(FeatureNotAvailable):
834
+ s.eval("c.mod.popen('id').read()")
835
+
836
+ def test_forbidden_function_direct_from_names(self):
837
+ """Forbidden functions passed directly in names should
838
+ be blocked when accessed"""
839
+ s = SimpleEval(names={"evil": exec})
840
+
841
+ with self.assertRaises(FeatureNotAvailable):
842
+ s.eval("evil")
843
+
844
+ def test_module_direct_from_names(self):
845
+ """Modules passed directly in names should be blocked
846
+ when accessed"""
847
+ import os
848
+
849
+ s = SimpleEval(names={"m": os})
850
+
851
+ with self.assertRaises(FeatureNotAvailable):
852
+ s.eval("m")
853
+
854
+ def test_forbidden_function_via_callable_name_handler(self):
855
+ """Forbidden functions from callable name handlers should
856
+ be blocked"""
857
+
858
+ def name_handler(node):
859
+ if node.id == "evil":
860
+ return exec
861
+
862
+ s = SimpleEval(names=name_handler)
863
+
864
+ with self.assertRaises(FeatureNotAvailable):
865
+ s.eval("evil")
866
+
867
+ def test_module_via_callable_name_handler(self):
868
+ """Modules from callable name handlers should be blocked"""
869
+ import os
870
+
871
+ def name_handler(node):
872
+ if node.id == "m":
873
+ return os
874
+
875
+ s = SimpleEval(names=name_handler)
876
+
877
+ with self.assertRaises(FeatureNotAvailable):
878
+ s.eval("m")
879
+
880
+ def test_forbidden_function_passed_to_custom_function(self):
881
+ """Passing forbidden functions to custom functions should be
882
+ blocked - they can be executed by the custom function"""
883
+
884
+ def evil_caller(func):
885
+ return func("print('pwned')") # pragma: no cover
886
+
887
+ s = SimpleEval(names={"evil": exec}, functions={"evil_caller": evil_caller})
888
+
889
+ with self.assertRaises(FeatureNotAvailable):
890
+ s.eval("evil_caller(evil)")
891
+
892
+ def test_module_passed_to_custom_function(self):
893
+ """Passing modules to custom functions should be blocked - they
894
+ can be used by the custom function"""
895
+ import os
896
+
897
+ def os_caller(mod):
898
+ return mod.system("id") # pragma: no cover
899
+
900
+ s = SimpleEval(names={"m": os}, functions={"os_caller": os_caller})
901
+
902
+ with self.assertRaises(FeatureNotAvailable):
903
+ s.eval("os_caller(m)") # pragma: no cover
904
+
905
+ def test_forbidden_function_in_list_passed_to_custom_function(self):
906
+ """Forbidden functions in containers passed to custom functions
907
+ should be blocked"""
908
+
909
+ def extract_and_call(items):
910
+ return items[0]("print('pwned')") # pragma: no cover
911
+
912
+ s = SimpleEval(names={"funcs": [exec, eval]}, functions={"extract": extract_and_call})
913
+
914
+ with self.assertRaises(FeatureNotAvailable):
915
+ s.eval("extract(funcs)")
916
+
917
+ def test_module_in_list_passed_to_custom_function(self):
918
+ """Modules in containers passed to custom functions should be
919
+ blocked"""
920
+ import os
921
+
922
+ def extract_and_use(items):
923
+ return items[0].system("id") # pragma: no cover
924
+
925
+ s = SimpleEval(names={"mods": [os.path, os]}, functions={"extract": extract_and_use})
926
+
927
+ with self.assertRaises(FeatureNotAvailable):
928
+ s.eval("extract(mods)")
929
+
930
+ def test_forbidden_function_in_dict_passed_to_custom_function(self):
931
+ """Forbidden functions in dicts passed to custom functions should
932
+ be blocked"""
933
+
934
+ def extract_and_call(d):
935
+ return d["bad"]("print('pwned')") # pragma: no cover
936
+
937
+ s = SimpleEval(
938
+ names={"funcs": {"bad": exec, "good": print}}, functions={"extract": extract_and_call}
939
+ )
940
+
941
+ with self.assertRaises(FeatureNotAvailable):
942
+ s.eval("extract(funcs)")
943
+
944
+ def test_module_in_dict_passed_to_custom_function(self):
945
+ """Modules in dicts passed to custom functions should be blocked"""
946
+ import os
947
+
948
+ def extract_and_use(d):
949
+ return d["m"].system("id") # pragma: no cover
950
+
951
+ s = SimpleEval(
952
+ names={"mods": {"m": os, "p": os.path}}, functions={"extract": extract_and_use}
953
+ )
954
+
955
+ with self.assertRaises(FeatureNotAvailable):
956
+ s.eval("extract(mods)")
957
+
958
+
959
+ class TestModuleWrapper(unittest.TestCase):
960
+ """Test the ModuleWrapper class itself"""
961
+
962
+ def test_module_wrapper_requires_module(self):
963
+ """ModuleWrapper should reject non-module types"""
964
+ with self.assertRaises(TypeError):
965
+ ModuleWrapper("not a module")
966
+
967
+ with self.assertRaises(TypeError):
968
+ ModuleWrapper(42)
969
+
970
+ with self.assertRaises(TypeError):
971
+ ModuleWrapper({})
972
+
973
+ def test_module_wrapper_allows_valid_module(self):
974
+ """ModuleWrapper should accept valid modules"""
975
+ import os.path
976
+
977
+ wrapper = ModuleWrapper(os.path)
978
+ self.assertIsNotNone(wrapper)
979
+
980
+ def test_module_wrapper_blocks_private_attrs(self):
981
+ """ModuleWrapper should block access to private attributes"""
982
+ import os.path
983
+
984
+ wrapper = ModuleWrapper(os.path)
985
+
986
+ with self.assertRaises(FeatureNotAvailable):
987
+ wrapper.__all__
988
+
989
+ with self.assertRaises(FeatureNotAvailable):
990
+ wrapper._internal
991
+
992
+ def test_module_wrapper_allows_public_attrs(self):
993
+ """ModuleWrapper should allow access to public attributes"""
994
+ import os.path
995
+
996
+ wrapper = ModuleWrapper(os.path)
997
+ # Should not raise
998
+ _ = wrapper.exists
999
+
1000
+ def test_module_wrapper_blocks_disallowed_methods(self):
1001
+ """ModuleWrapper should block access to methods in DISALLOW_METHODS"""
1002
+ import os
1003
+
1004
+ wrapper = ModuleWrapper(os)
1005
+
1006
+ with self.assertRaises(FeatureNotAvailable):
1007
+ wrapper.mro
1008
+
1009
+ def test_module_wrapper_with_allowed_attrs_allows_whitelisted(self):
1010
+ """ModuleWrapper with allowed_attrs should allow whitelisted
1011
+ attributes"""
1012
+ import os.path
1013
+
1014
+ wrapper = ModuleWrapper(os.path, allowed_attrs={"exists", "join"})
1015
+
1016
+ # Should not raise
1017
+ _ = wrapper.exists
1018
+ _ = wrapper.join
1019
+
1020
+ def test_module_wrapper_with_allowed_attrs_blocks_non_whitelisted(self):
1021
+ """ModuleWrapper with allowed_attrs should block non-whitelisted
1022
+ attributes"""
1023
+ import os.path
1024
+
1025
+ wrapper = ModuleWrapper(os.path, allowed_attrs={"exists"})
1026
+
1027
+ with self.assertRaises(FeatureNotAvailable):
1028
+ wrapper.join
1029
+
1030
+ def test_module_wrapper_getattr_returns_actual_attribute(self):
1031
+ """ModuleWrapper.__getattr__ should return the actual module
1032
+ attribute"""
1033
+ import os.path
1034
+
1035
+ wrapper = ModuleWrapper(os.path)
1036
+ result = wrapper.exists
1037
+
1038
+ # Should be the actual function
1039
+ self.assertEqual(result, os.path.exists)
1040
+
1041
+
1042
+ class TestModuleWrapperAccess(DRYTest):
1043
+ """Test ModuleWrapper integration with SimpleEval"""
1044
+
1045
+ def test_unwrapped_module_blocked(self):
1046
+ """Unwrapped modules in names should be blocked"""
1047
+ import os.path
1048
+
1049
+ s = SimpleEval(names={"path": os.path})
1050
+
1051
+ with self.assertRaises(FeatureNotAvailable):
1052
+ s.eval("path")
1053
+
1054
+ def test_wrapped_module_allowed(self):
1055
+ """ModuleWrapper should allow module access in eval"""
1056
+ import os.path
1057
+
1058
+ s = SimpleEval(names={"path": ModuleWrapper(os.path)})
1059
+
1060
+ result = s.eval("path.exists('/etc/passwd')")
1061
+ self.assertTrue(isinstance(result, bool))
1062
+
1063
+ def test_wrapped_module_private_attrs_blocked(self):
1064
+ """ModuleWrapper should block private attrs in eval"""
1065
+ import os.path
1066
+
1067
+ s = SimpleEval(names={"path": ModuleWrapper(os.path)})
1068
+
1069
+ with self.assertRaises(FeatureNotAvailable):
1070
+ s.eval("path.__all__")
1071
+
1072
+ def test_wrapped_module_with_whitelist(self):
1073
+ """ModuleWrapper with whitelist should allow whitelisted attrs"""
1074
+ import os.path
1075
+
1076
+ s = SimpleEval(names={"path": ModuleWrapper(os.path, allowed_attrs={"exists"})})
1077
+
1078
+ result = s.eval("path.exists('/etc/passwd')")
1079
+ self.assertTrue(isinstance(result, bool))
1080
+
1081
+ def test_wrapped_module_with_whitelist_blocks_others(self):
1082
+ """ModuleWrapper with whitelist should block non-whitelisted
1083
+ attrs"""
1084
+ import os.path
1085
+
1086
+ s = SimpleEval(names={"path": ModuleWrapper(os.path, allowed_attrs={"exists"})})
1087
+
1088
+ with self.assertRaises(FeatureNotAvailable):
1089
+ s.eval("path.join('a', 'b')")
1090
+
1091
+ def test_wrapped_module_passed_to_function(self):
1092
+ """ModuleWrapper can be passed to custom functions"""
1093
+
1094
+ def process_path(path_mod):
1095
+ return path_mod.exists("/etc/passwd")
1096
+
1097
+ import os.path
1098
+
1099
+ s = SimpleEval(names={"path": ModuleWrapper(os.path)}, functions={"process": process_path})
1100
+
1101
+ result = s.eval("process(path)")
1102
+ self.assertTrue(isinstance(result, bool))
1103
+
1104
+ def test_wrapped_module_in_container(self):
1105
+ """ModuleWrapper can be stored in containers"""
1106
+ import os.path
1107
+
1108
+ s = SimpleEval(names={"items": [ModuleWrapper(os.path), 1, 2]})
1109
+
1110
+ result = s.eval("items")
1111
+ self.assertEqual(len(result), 3)
1112
+
1113
+ def test_wrapped_module_in_dict_container(self):
1114
+ """ModuleWrapper can be stored in dicts"""
1115
+ import os.path
1116
+
1117
+ s = SimpleEval(names={"data": {"path": ModuleWrapper(os.path), "value": 42}})
1118
+
1119
+ result = s.eval("data['value']")
1120
+ self.assertEqual(result, 42)
1121
+
593
1122
 
594
1123
  class TestCompoundTypes(DRYTest):
595
1124
  """Test the compound-types edition of the library"""
@@ -1250,33 +1779,37 @@ class TestShortCircuiting(DRYTest):
1250
1779
 
1251
1780
 
1252
1781
  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
1782
+ def test_functions_in_disallowed_functions_list(self):
1783
+ # a bit of double-entry testing. probably pointless.
1784
+ assert simpleeval.DISALLOW_FUNCTIONS.issuperset(
1785
+ {
1786
+ type,
1787
+ isinstance,
1788
+ eval,
1789
+ getattr,
1790
+ setattr,
1791
+ help,
1792
+ repr,
1793
+ compile,
1794
+ open,
1795
+ exec,
1796
+ os.popen,
1797
+ os.system,
1798
+ }
1799
+ )
1258
1800
 
1259
- for x in DISALLOWED:
1801
+ def test_functions_are_disallowed_at_init(self):
1802
+ for dangerous_function in simpleeval.DISALLOW_FUNCTIONS:
1260
1803
  with self.assertRaises(FeatureNotAvailable):
1261
- SimpleEval(functions={"foo": x})
1804
+ SimpleEval(functions={"foo": dangerous_function})
1262
1805
 
1263
1806
  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()
1807
+ for dangerous_function in simpleeval.DISALLOW_FUNCTIONS:
1273
1808
  with self.assertRaises(FeatureNotAvailable):
1274
1809
  s = SimpleEval()
1275
- s.functions["foo"] = x
1810
+ s.functions["foo"] = dangerous_function
1276
1811
  s.eval("foo(42)")
1277
1812
 
1278
- simpleeval.DEFAULT_FUNCTIONS = DF.copy()
1279
-
1280
1813
  def test_breakout_via_generator(self):
1281
1814
  # Thanks decorator-factory
1282
1815
  class Foo:
File without changes
File without changes