diff --git a/frappe/tests/test_safe_exec.py b/frappe/tests/test_safe_exec.py index caa98737a2..fcd5832680 100644 --- a/frappe/tests/test_safe_exec.py +++ b/frappe/tests/test_safe_exec.py @@ -1,3 +1,5 @@ +import types + import frappe from frappe.tests.utils import FrappeTestCase from frappe.utils.safe_exec import get_safe_globals, safe_exec @@ -59,3 +61,17 @@ class TestSafeExec(FrappeTestCase): # enqueue whitelisted method safe_exec("""frappe.enqueue("ping", now=True)""") + + def test_ensure_getattrable_globals(self): + def check_safe(objects): + for obj in objects: + if isinstance(obj, (types.ModuleType, types.CodeType, types.TracebackType, types.FrameType)): + self.fail(f"{obj} wont work in safe exec.") + elif isinstance(obj, dict): + check_safe(obj.values()) + + check_safe(get_safe_globals().values()) + + def test_unsafe_objects(self): + unsafe_global = {"frappe": frappe} + self.assertRaises(SyntaxError, safe_exec, """frappe.msgprint("Hello")""", unsafe_global) diff --git a/frappe/utils/safe_exec.py b/frappe/utils/safe_exec.py index afdd4694a8..c75a5fd12b 100644 --- a/frappe/utils/safe_exec.py +++ b/frappe/utils/safe_exec.py @@ -2,6 +2,9 @@ import copy import inspect import json import mimetypes +import types +from contextlib import contextmanager +from functools import lru_cache import RestrictedPython.Guards from RestrictedPython import compile_restricted, safe_globals @@ -64,14 +67,20 @@ def safe_exec(script, _globals=None, _locals=None, restrict_commit_rollback=Fals exec_globals.frappe.db.pop("rollback", None) exec_globals.frappe.db.pop("add_index", None) - # execute script compiled by RestrictedPython - frappe.flags.in_safe_exec = True - exec(compile_restricted(script), exec_globals, _locals) # pylint: disable=exec-used - frappe.flags.in_safe_exec = False + with safe_exec_flags(), patched_qb(): + # execute script compiled by RestrictedPython + exec(compile_restricted(script), exec_globals, _locals) # pylint: disable=exec-used return exec_globals, _locals +@contextmanager +def safe_exec_flags(): + frappe.flags.in_safe_exec = True + yield + frappe.flags.in_safe_exec = False + + def get_safe_globals(): datautils = frappe._dict() @@ -258,6 +267,24 @@ def call_with_form_dict(function, kwargs): frappe.local.form_dict = form_dict +@contextmanager +def patched_qb(): + try: + _terms = frappe.qb.terms + frappe.qb.terms = _flatten(frappe.qb.terms) + yield + finally: + frappe.qb.terms = _terms + + +@lru_cache +def _flatten(module): + new_mod = NamespaceDict() + for name, obj in inspect.getmembers(module, lambda x: not inspect.ismodule(x)): + new_mod[name] = obj + return new_mod + + def get_python_builtins(): return { "abs": abs, @@ -350,6 +377,10 @@ def _getattr(object, name, default=None): if isinstance(name, str) and (name in UNSAFE_ATTRIBUTES): raise SyntaxError(f"{name} is an unsafe attribute") + + if isinstance(object, (types.ModuleType, types.CodeType, types.TracebackType, types.FrameType)): + raise SyntaxError(f"Reading {object} attributes is not allowed") + return RestrictedPython.Guards.safer_getattr(object, name, default=default)