Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--source/blender/python/intern/bpy.c53
-rw-r--r--tests/python/CMakeLists.txt5
-rw-r--r--tests/python/bl_pyapi_bpy_driver_secure_eval.py220
3 files changed, 278 insertions, 0 deletions
diff --git a/source/blender/python/intern/bpy.c b/source/blender/python/intern/bpy.c
index 2e97ae0fc1d..d1e8b894ac0 100644
--- a/source/blender/python/intern/bpy.c
+++ b/source/blender/python/intern/bpy.c
@@ -32,6 +32,7 @@
#include "bpy.h"
#include "bpy_app.h"
#include "bpy_capi_utils.h"
+#include "bpy_driver.h"
#include "bpy_library.h"
#include "bpy_operator.h"
#include "bpy_props.h"
@@ -326,6 +327,49 @@ static PyObject *bpy_resource_path(PyObject *UNUSED(self), PyObject *args, PyObj
return PyC_UnicodeFromByte(path ? path : "");
}
+/* This is only exposed for tests, see: `tests/python/bl_pyapi_bpy_driver_secure_eval.py`. */
+PyDoc_STRVAR(bpy_driver_secure_code_test_doc,
+ ".. function:: _driver_secure_code_test(code)\n"
+ "\n"
+ " Test if the script should be considered trusted.\n"
+ "\n"
+ " :arg code: The code to test.\n"
+ " :type code: code\n"
+ " :arg namespace: The namespace of values which are allowed.\n"
+ " :type namespace: dict\n"
+ " :arg verbose: Print the reason for considering insecure to the ``stderr``.\n"
+ " :type verbose: bool\n"
+ " :return: True when the script is considered trusted.\n"
+ " :rtype: bool\n");
+static PyObject *bpy_driver_secure_code_test(PyObject *UNUSED(self), PyObject *args, PyObject *kw)
+{
+ PyObject *py_code;
+ PyObject *py_namespace = NULL;
+ const bool verbose = false;
+ static const char *_keywords[] = {"code", "namespace", "verbose", NULL};
+ static _PyArg_Parser _parser = {
+ "O!" /* `expression` */
+ "|$" /* Optional keyword only arguments. */
+ "O!" /* `namespace` */
+ "O&" /* `verbose` */
+ ":driver_secure_code_test",
+ _keywords,
+ 0,
+ };
+ if (!_PyArg_ParseTupleAndKeywordsFast(args,
+ kw,
+ &_parser,
+ &PyCode_Type,
+ &py_code,
+ &PyDict_Type,
+ &py_namespace,
+ PyC_ParseBool,
+ &verbose)) {
+ return NULL;
+ }
+ return PyBool_FromLong(BPY_driver_secure_bytecode_test(py_code, py_namespace, verbose));
+}
+
PyDoc_STRVAR(bpy_escape_identifier_doc,
".. function:: escape_identifier(string)\n"
"\n"
@@ -528,6 +572,12 @@ static PyMethodDef meth_bpy_resource_path = {
METH_VARARGS | METH_KEYWORDS,
bpy_resource_path_doc,
};
+static PyMethodDef meth_bpy_driver_secure_code_test = {
+ "_driver_secure_code_test",
+ (PyCFunction)bpy_driver_secure_code_test,
+ METH_VARARGS | METH_KEYWORDS,
+ bpy_driver_secure_code_test_doc,
+};
static PyMethodDef meth_bpy_escape_identifier = {
"escape_identifier",
(PyCFunction)bpy_escape_identifier,
@@ -648,6 +698,9 @@ void BPy_init_modules(struct bContext *C)
meth_bpy_resource_path.ml_name,
(PyObject *)PyCFunction_New(&meth_bpy_resource_path, NULL));
PyModule_AddObject(mod,
+ meth_bpy_driver_secure_code_test.ml_name,
+ (PyObject *)PyCFunction_New(&meth_bpy_driver_secure_code_test, NULL));
+ PyModule_AddObject(mod,
meth_bpy_escape_identifier.ml_name,
(PyObject *)PyCFunction_New(&meth_bpy_escape_identifier, NULL));
PyModule_AddObject(mod,
diff --git a/tests/python/CMakeLists.txt b/tests/python/CMakeLists.txt
index 38c3fc4389a..d95f2cd2644 100644
--- a/tests/python/CMakeLists.txt
+++ b/tests/python/CMakeLists.txt
@@ -102,6 +102,11 @@ add_blender_test(
)
add_blender_test(
+ script_pyapi_bpy_driver_secure_eval
+ --python ${CMAKE_CURRENT_LIST_DIR}/bl_pyapi_bpy_driver_secure_eval.py
+)
+
+add_blender_test(
script_pyapi_idprop
--python ${CMAKE_CURRENT_LIST_DIR}/bl_pyapi_idprop.py
)
diff --git a/tests/python/bl_pyapi_bpy_driver_secure_eval.py b/tests/python/bl_pyapi_bpy_driver_secure_eval.py
new file mode 100644
index 00000000000..953dbcd5381
--- /dev/null
+++ b/tests/python/bl_pyapi_bpy_driver_secure_eval.py
@@ -0,0 +1,220 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+# ./blender.bin --background -noaudio --python tests/python/bl_pyapi_bpy_driver_secure_eval.py -- --verbose
+import bpy
+import unittest
+import builtins
+from types import ModuleType
+
+
+# -----------------------------------------------------------------------------
+# Mock Environment
+
+
+expect_unreachable_msg = "This function should _NEVER_ run!"
+# Internal check, to ensure this actually runs as expected.
+expect_unreachable_count = 0
+
+
+def expect_os_unreachable():
+ global expect_unreachable_count
+ expect_unreachable_count += 1
+ raise Exception(expect_unreachable_msg)
+
+
+__import__("os").expect_os_unreachable = expect_os_unreachable
+
+
+expect_open_unreachable_count = 0
+
+
+def open_expect_unreachable(*args, **kwargs):
+ global expect_open_unreachable_count
+ expect_open_unreachable_count += 1
+ raise Exception(expect_unreachable_msg)
+
+
+mock_builtins = {**builtins.__dict__, **{"open": open_expect_unreachable}}
+
+
+# -----------------------------------------------------------------------------
+# Utility Functions
+
+
+def is_expression_secure(expr_str, verbose):
+ """
+ Return (ok, code) where ok is true if expr_str is considered secure.
+ """
+ # Internal function only for testing (not part of the public API).
+ from _bpy import _driver_secure_code_test
+ expr_code = compile(expr_str, "<is_expression_secure>", 'eval')
+ ok = _driver_secure_code_test(expr_code, verbose=verbose)
+ return ok, expr_code
+
+
+# -----------------------------------------------------------------------------
+# Tests (Accept)
+
+
+class _TestExprMixIn:
+ """
+ Sub-classes must define:
+ - expressions_expect_secure: boolean, the expected secure state.
+ - expressions: A sequence of expressions that must evaluate in the driver name-space.
+
+ Optionally:
+ - expressions_expect_unreachable:
+ A boolean, when true, it's expected each expression should call
+ ``expect_os_unreachable`` or ``expect_open_unreachable``.
+ """
+
+ # Sub-class may override.
+ expressions_expect_unreachable = False
+
+ def assertSecure(self, expect_secure, expr_str):
+ is_secure, expr_code = is_expression_secure(
+ expr_str,
+ # Only verbose when secure as this is will result in an failure,
+ # in that case it's useful to know which op-codes caused the test to unexpectedly fail.
+ verbose=expect_secure,
+ )
+ if is_secure != expect_secure:
+ raise self.failureException(
+ "Expression \"%s\" was expected to be %s" %
+ (expr_str, "secure" if expect_secure else "insecure"))
+ # NOTE: executing is not essential, it's just better to ensure the expressions make sense.
+ try:
+ exec(
+ expr_code,
+ {"__builtins__": mock_builtins},
+ {**bpy.app.driver_namespace, **{"__builtins__": mock_builtins}},
+ )
+ # exec(expr_code, {}, bpy.app.driver_namespace)
+ ex = None
+ except BaseException as ex_test:
+ ex = ex_test
+
+ if self.expressions_expect_unreachable:
+ if ex and ex.args == (expect_unreachable_msg,):
+ ex = None
+ elif not ex:
+ raise self.failureException("Expression \"%s\" failed to run `os.expect_os_unreachable`" % (expr_str,))
+ else:
+ # An unknown exception was raised, use the exception below.
+ pass
+
+ if ex:
+ raise self.failureException("Expression \"%s\" failed to evaluate with error: %r" % (expr_str, ex))
+
+ def test_expr(self):
+ expect_secure = self.expressions_expect_secure
+ for expr_str in self.expressions:
+ self.assertSecure(expect_secure, expr_str)
+
+
+class TestExprMixIn_Accept(_TestExprMixIn):
+ expressions_expect_secure = True
+
+
+class TestExprMixIn_Reject(_TestExprMixIn):
+ expressions_expect_secure = False
+
+
+class TestAcceptLiteralNumbers(unittest.TestCase, TestExprMixIn_Accept):
+ expressions = ("1", "1_1", "1.1", "1j", "0x1", "0o1", "0b1")
+
+
+class TestAcceptLiteralStrings(unittest.TestCase, TestExprMixIn_Accept):
+ expressions = ("''", "'_'", "r''", "r'_'", "'''_'''")
+
+
+class TestAcceptSequencesEmpty(unittest.TestCase, TestExprMixIn_Accept):
+ expressions = ("()", "[]", "{}", "[[]]", "(())")
+
+
+class TestAcceptSequencesSimple(unittest.TestCase, TestExprMixIn_Accept):
+ expressions = ("('', '')", "['', '_']", "{'', '_'}", "{'': '_'}")
+
+
+class TestAcceptSequencesExpand(unittest.TestCase, TestExprMixIn_Accept):
+ expressions = ("(*('', '_'),)", "[*(), *[]]", "{*{1, 2}}")
+
+
+class TestAcceptSequencesComplex(unittest.TestCase, TestExprMixIn_Accept):
+ expressions = ("[1, 2, 3][-1:0:-1][0]", "1 in (1, 2)", "False if 1 in {1, 2} else True")
+
+
+class TestAcceptMathOperators(unittest.TestCase, TestExprMixIn_Accept):
+ expressions = ("4 / 4", "4 * 4", "4 // 4", "2 ** 2", "4 ^ -1", "4 & 1", "4 % 1")
+
+
+class TestAcceptMathFunctionsSimple(unittest.TestCase, TestExprMixIn_Accept):
+ expressions = ("sin(pi)", "degrees(pi / 2)", "clamp(4, 0, 1)")
+
+
+class TestAcceptMathFunctionsComplex(unittest.TestCase, TestExprMixIn_Accept):
+ expressions = ("-(sin(pi) ** 2) / 2", "floor(22 / 7)", "ceil(pi + 1)")
+
+
+# -----------------------------------------------------------------------------
+# Tests (Reject)
+
+class TestRejectLiteralFStrings(unittest.TestCase, TestExprMixIn_Reject):
+ # F-String's are not supported as `BUILD_STRING` op-code is disabled,
+ # while it may be safe to enable that needs to be double-checked.
+ # Further it doesn't seem useful for typical math expressions used in drivers.
+ expressions = ("f''", "f'{1}'", "f'{\"_\"}'")
+
+
+class TestRejectModuleAccess(unittest.TestCase, TestExprMixIn_Reject):
+ # Each of these commands _must_ run `expect_os_unreachable`,
+ # and must also be rejected as insecure - otherwise we have problems.
+ expressions_expect_unreachable = True
+ expressions = (
+ "__import__('os').expect_os_unreachable()",
+ "exec(\"__import__('os').expect_os_unreachable()\")",
+ "(globals().update(__import__('os').__dict__), expect_os_unreachable())",
+ )
+
+ # Ensure the functions are actually called.
+ def setUp(self):
+ self._count = expect_unreachable_count
+
+ def tearDown(self):
+ count_actual = expect_unreachable_count - self._count
+ count_expect = len(self.expressions)
+ if count_actual != count_expect:
+ raise Exception(
+ "Expected 'os.expect_os_unreachable' to be called %d times but was called %d times" %
+ (count_expect, count_actual),
+ )
+
+
+class TestRejectOpenAccess(unittest.TestCase, TestExprMixIn_Reject):
+ # Each of these commands _must_ run `expect_open_unreachable`,
+ # and must also be rejected as insecure - otherwise we have problems.
+ expressions_expect_unreachable = True
+ expressions = (
+ "open('file.txt', 'r')",
+ "exec(\"open('file.txt', 'r')\")",
+ "(globals().update({'fake_open': __builtins__['open']}), fake_open())",
+ )
+
+ # Ensure the functions are actually called.
+ def setUp(self):
+ self._count = expect_open_unreachable_count
+
+ def tearDown(self):
+ count_actual = expect_open_unreachable_count - self._count
+ count_expect = len(self.expressions)
+ if count_actual != count_expect:
+ raise Exception(
+ "Expected 'open' to be called %d times but was called %d times" %
+ (count_expect, count_actual),
+ )
+
+
+if __name__ == '__main__':
+ import sys
+ sys.argv = [__file__] + (sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [])
+ unittest.main()