mirror of
https://github.com/openembedded/meta-openembedded.git
synced 2026-09-09 18:50:13 +00:00
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>
188 lines
6.5 KiB
Diff
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
|
|
|