fix: Show server script name in traceback (#23676)

* fix: Show server script name in traceback

* chore: typo

Co-authored-by: Sagar Vora <sagar@resilient.tech>

---------

Co-authored-by: Sagar Vora <sagar@resilient.tech>
This commit is contained in:
Ankush Menat 2023-12-08 15:01:13 +05:30 committed by GitHub
parent b98c550823
commit 57699a54b1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 47 additions and 26 deletions

View file

@ -184,7 +184,7 @@ class Report(Document):
def execute_script(self, filters):
# server script
loc = {"filters": frappe._dict(filters), "data": None, "result": None}
safe_exec(self.report_script, None, loc)
safe_exec(self.report_script, None, loc, script_filename=f"Report {self.name}")
if loc["data"]:
return loc["data"]
else:

View file

@ -155,7 +155,12 @@ class ServerScript(Document):
Args:
doc (Document): Executes script with for a certain document's events
"""
safe_exec(self.script, _locals={"doc": doc}, restrict_commit_rollback=True)
safe_exec(
self.script,
_locals={"doc": doc},
restrict_commit_rollback=True,
script_filename=self.name,
)
def execute_scheduled_method(self):
"""Specific to Scheduled Jobs via Server Scripts
@ -166,7 +171,7 @@ class ServerScript(Document):
if self.script_type != "Scheduler Event":
raise frappe.DoesNotExistError
safe_exec(self.script)
safe_exec(self.script, script_filename=self.name)
def get_permission_query_conditions(self, user: str) -> list[str]:
"""Specific to Permission Query Server Scripts
@ -178,7 +183,7 @@ class ServerScript(Document):
list: Returns list of conditions defined by rules in self.script
"""
locals = {"user": user, "conditions": ""}
safe_exec(self.script, None, locals)
safe_exec(self.script, None, locals, script_filename=self.name)
if locals["conditions"]:
return locals["conditions"]
@ -278,7 +283,7 @@ def execute_api_server_script(script=None, *args, **kwargs):
raise frappe.PermissionError
# output can be stored in flags
_globals, _locals = safe_exec(script.script)
_globals, _locals = safe_exec(script.script, script_filename=script.name)
return _globals.frappe.flags

View file

@ -28,7 +28,7 @@ class SystemConsole(Document):
try:
frappe.local.debug_log = []
if self.type == "Python":
safe_exec(self.console)
safe_exec(self.console, script_filename="System Console")
self.output = "\n".join(frappe.debug_log)
elif self.type == "SQL":
self.output = frappe.as_json(read_sql(self.console, as_dict=1))

View file

@ -107,6 +107,8 @@ def patch_query_execute():
def prepare_query(query):
import inspect
from frappe.utils.safe_exec import SERVER_SCRIPT_FILE_PREFIX
param_collector = NamedParameterWrapper()
query = query.get_sql(param_wrapper=param_collector)
if frappe.flags.in_safe_exec:
@ -114,21 +116,20 @@ def patch_query_execute():
if not check_safe_sql_query(query, throw=False):
callstack = inspect.stack()
if len(callstack) >= 3 and ".py" in callstack[2].filename:
# ignore any query builder methods called from python files
# assumption is that those functions are whitelisted already.
# since query objects are patched everywhere any query.run()
# will have callstack like this:
# frame0: this function prepare_query()
# frame1: execute_query()
# frame2: frame that called `query.run()`
#
# if frame2 is server script <serverscript> is set as the filename
# it shouldn't be allowed.
pass
else:
# This check is required because QB can execute from anywhere and we can not
# reliably provide a safe version for it in server scripts.
# since query objects are patched everywhere any query.run()
# will have callstack like this:
# frame0: this function prepare_query()
# frame1: execute_query()
# frame2: frame that called `query.run()`
#
# if frame2 is server script <serverscript> is set as the filename it shouldn't be allowed.
if len(callstack) >= 3 and SERVER_SCRIPT_FILE_PREFIX in callstack[2].filename:
raise frappe.PermissionError("Only SELECT SQL allowed in scripting")
return query, param_collector.get_parameters()
builder_class = frappe.qb._BuilderClasss

View file

@ -41,11 +41,13 @@ def sql(*args, **kwargs):
def get_current_stack_frames():
from frappe.utils.safe_exec import SERVER_SCRIPT_FILE_PREFIX
try:
current = inspect.currentframe()
frames = inspect.getouterframes(current, context=10)
for frame, filename, lineno, function, context, index in list(reversed(frames))[:-2]:
if "/apps/" in filename or "<serverscript>" in filename:
if "/apps/" in filename or SERVER_SCRIPT_FILE_PREFIX in filename:
yield {
"filename": TRACEBACK_PATH_PATTERN.sub("", filename),
"lineno": lineno,

View file

@ -146,19 +146,20 @@ def guess_exception_source(exception: str) -> str | None:
- For unhandled exception last python file from apps folder is responsible.
- For frappe.throws the exception source is possibly present after skipping frappe.throw frames
- For server script the file name is `<serverscript>`
- For server script the file name contains SERVER_SCRIPT_FILE_PREFIX
"""
from frappe.utils.safe_exec import SERVER_SCRIPT_FILE_PREFIX
with suppress(Exception):
installed_apps = frappe.get_installed_apps()
app_priority = {app: installed_apps.index(app) for app in installed_apps}
APP_NAME_REGEX = re.compile(r".*File.*apps/(?P<app_name>\w+)/\1/")
SERVER_SCRIPT_FRAME = re.compile(r".*<serverscript>")
apps = Counter()
for line in reversed(exception.splitlines()):
if SERVER_SCRIPT_FRAME.match(line):
if SERVER_SCRIPT_FILE_PREFIX in line:
return "Server Script"
if matches := APP_NAME_REGEX.match(line):

View file

@ -37,6 +37,7 @@ class ServerScriptNotEnabled(frappe.PermissionError):
ARGUMENT_NOT_SET = object()
SAFE_EXEC_CONFIG_KEY = "server_script_enabled"
SERVER_SCRIPT_FILE_PREFIX = "<serverscript>"
class NamespaceDict(frappe._dict):
@ -76,7 +77,14 @@ def is_safe_exec_enabled() -> bool:
return bool(frappe.get_common_site_config().get(SAFE_EXEC_CONFIG_KEY))
def safe_exec(script, _globals=None, _locals=None, restrict_commit_rollback=False):
def safe_exec(
script: str,
_globals: dict | None = None,
_locals: dict | None = None,
*,
restrict_commit_rollback: bool = False,
script_filename: str | None = None,
):
if not is_safe_exec_enabled():
msg = _("Server Scripts are disabled. Please enable server scripts from bench configuration.")
@ -95,10 +103,14 @@ 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)
filename = SERVER_SCRIPT_FILE_PREFIX
if script_filename:
filename += f": {frappe.scrub(script_filename)}"
with safe_exec_flags(), patched_qb():
# execute script compiled by RestrictedPython
exec(
compile_restricted(script, filename="<serverscript>", policy=FrappeTransformer),
compile_restricted(script, filename=filename, policy=FrappeTransformer),
exec_globals,
_locals,
)

View file

@ -78,7 +78,7 @@ class WebPage(WebsiteGenerator):
if self.context_script:
_locals = dict(context=frappe._dict())
safe_exec(self.context_script, None, _locals)
safe_exec(self.context_script, None, _locals, script_filename=f"web page {self.name}")
context.update(_locals["context"])
self.render_dynamic(context)