Files
meta-openembedded/meta-python/recipes-devtools/python/python3-simpleeval/CVE-2026-32640_p2.patch
T
Hetvi Thakar 6752204aad python3-simpleeval: Fix CVE-2026-32640
Manually backport the three upstream security fixes for
CVE-2026-32640 to the Scarthgap simpleeval 0.9.13 recipe [1][2][3].

Include the required unhashable-container correction [4], which
prevents the recursive security checks from raising TypeError on
legitimate list and tuple values.

Harden the recursive callback-argument validation to inspect sets,
frozensets, and dictionary keys, and safely handle cyclic containers.
Add regression coverage for each of these cases.

Do not include the separate generator/coroutine hardening or the
optional performance follow-up. Omit the new ModuleWrapper API so this
stable-branch fix adds no unrelated public feature.

[1] https://github.com/danthedeckie/simpleeval/commit/9cb4a7b99498
[2] https://github.com/danthedeckie/simpleeval/commit/1654cbf02193
[3] https://github.com/danthedeckie/simpleeval/commit/cffa9f68cee5
[4] https://github.com/danthedeckie/simpleeval/commit/d1e4569db678

Signed-off-by: Hetvi Thakar <hthakar@cisco.com>
Signed-off-by: Anuj Mittal <anuj.mittal@oss.qualcomm.com>
2026-09-01 06:57:22 +05:30

188 lines
6.5 KiB
Diff

From bf7d5c05015cdbf8af0d298f286e76e9a6abad7c Mon Sep 17 00:00:00 2001
From: Daniel Fairhead <daniel@dev.ngo>
Date: Thu, 12 Mar 2026 09:37:22 +0000
Subject: [PATCH 2/4] Disallow module access & disallowed function access via
attributes.
CVE: CVE-2026-32640
Upstream-Status: Backport [https://github.com/danthedeckie/simpleeval/commit/1654cbf0219345f707c79664b8657be6b8d23e33]
Backport Changes:
- Import only Hashable from typing because simpleeval 0.9.13 does not contain the later allowed_attrs type API and its Type, Dict, Set, and Union imports.
- Adapt TestDisallowedFunctions for 0.9.13's Python-2-compatible DISALLOWED setup: remove its two PYTHON3/dynamic-exec blocks; the upstream baseline already listed exec directly.
(cherry picked from commit 1654cbf0219345f707c79664b8657be6b8d23e33)
Signed-off-by: Hetvi Thakar <hthakar@cisco.com>
---
simpleeval.py | 31 +++++++++++++------
test_simpleeval.py | 77 ++++++++++++++++++++++++++++++----------------
2 files changed, 73 insertions(+), 35 deletions(-)
diff --git a/simpleeval.py b/simpleeval.py
index 91ed582..69aed83 100644
--- a/simpleeval.py
+++ b/simpleeval.py
@@ -100,8 +100,10 @@ import ast
import operator as op
import os
import sys
+import types
import warnings
from random import random
+from typing import Hashable
PYTHON3 = sys.version_info[0] == 3
PYTHON35 = PYTHON3 and sys.version_info > (3, 5)
@@ -240,6 +242,10 @@ class MultipleExpressions(UserWarning):
pass
+# Sentinal used during attr access
+_ATTR_NOT_FOUND = object()
+
+
########################################
# Default simple functions to include:
@@ -603,18 +609,25 @@ class SimpleEval(object): # pylint: disable=too-few-public-methods
# 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)
diff --git a/test_simpleeval.py b/test_simpleeval.py
index bc83d50..c60ec51 100644
--- a/test_simpleeval.py
+++ b/test_simpleeval.py
@@ -590,6 +590,34 @@ 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')")
+
class TestCompoundTypes(DRYTest):
"""Test the compound-types edition of the library"""
@@ -1199,40 +1227,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")
--
2.35.6