diff --git a/hrms/payroll/doctype/salary_slip/salary_slip.py b/hrms/payroll/doctype/salary_slip/salary_slip.py index dbaf4f796e..0dd7162ed5 100644 --- a/hrms/payroll/doctype/salary_slip/salary_slip.py +++ b/hrms/payroll/doctype/salary_slip/salary_slip.py @@ -2,6 +2,7 @@ # License: GNU General Public License v3. See license.txt +import unicodedata from datetime import date import frappe @@ -1047,14 +1048,14 @@ def eval_condition_and_formula(self, struct_row, data): try: condition = sanitize_expression(struct_row.condition) if condition: - if not frappe.safe_eval(condition, self.whitelisted_globals, data): + if not _safe_eval(condition, self.whitelisted_globals, data): return None amount = struct_row.amount if struct_row.amount_based_on_formula: formula = sanitize_expression(struct_row.formula) if formula: amount = flt( - frappe.safe_eval(formula, self.whitelisted_globals, data), struct_row.precision("amount") + _safe_eval(formula, self.whitelisted_globals, data), struct_row.precision("amount") ) if amount: data[struct_row.abbr] = amount @@ -2235,3 +2236,51 @@ def throw_error_message(row, error, title, description=None): ).format(**data) frappe.throw(message, title=title) + + +def _safe_eval(code: str, eval_globals: dict | None = None, eval_locals: dict | None = None): + """Old version of safe_eval from framework. + + Note: current frappe.safe_eval transforms code so if you have nested + iterations with too much depth then it can hit recursion limit of python. + There's no workaround for this and people need large formulas in some + countries so this is alternate implementation for that. + + WARNING: DO NOT use this function anywhere else outside of this file. + """ + code = unicodedata.normalize("NFKC", code) + + _check_attributes(code) + + whitelisted_globals = {"int": int, "float": float, "long": int, "round": round} + if not eval_globals: + eval_globals = {} + + eval_globals["__builtins__"] = {} + eval_globals.update(whitelisted_globals) + return eval(code, eval_globals, eval_locals) # nosemgrep + + +def _check_attributes(code: str) -> None: + import ast + + from frappe.utils.safe_exec import UNSAFE_ATTRIBUTES + + unsafe_attrs = set(UNSAFE_ATTRIBUTES).union(["__"]) - {"format"} + + for attribute in unsafe_attrs: + if attribute in code: + raise SyntaxError(f'Illegal rule {frappe.bold(code)}. Cannot use "{attribute}"') + + BLOCKED_NODES = (ast.NamedExpr,) + + tree = ast.parse(code, mode="eval") + for node in ast.walk(tree): + if isinstance(node, BLOCKED_NODES): + raise SyntaxError(f"Operation not allowed: line {node.lineno} column {node.col_offset}") + if ( + isinstance(node, ast.Attribute) + and isinstance(node.attr, str) + and node.attr in UNSAFE_ATTRIBUTES + ): + raise SyntaxError(f'Illegal rule {frappe.bold(code)}. Cannot use "{node.attr}"') diff --git a/hrms/payroll/doctype/salary_slip/test_salary_slip.py b/hrms/payroll/doctype/salary_slip/test_salary_slip.py index 7abe17b25d..3097347332 100644 --- a/hrms/payroll/doctype/salary_slip/test_salary_slip.py +++ b/hrms/payroll/doctype/salary_slip/test_salary_slip.py @@ -35,7 +35,10 @@ create_payroll_period, ) from hrms.payroll.doctype.payroll_entry.payroll_entry import get_month_details -from hrms.payroll.doctype.salary_slip.salary_slip import make_salary_slip_from_timesheet +from hrms.payroll.doctype.salary_slip.salary_slip import ( + _safe_eval, + make_salary_slip_from_timesheet, +) from hrms.payroll.doctype.salary_structure.salary_structure import make_salary_slip from hrms.tests.test_utils import get_first_sunday @@ -1387,6 +1390,41 @@ def test_variable_tax_component(self): self.assertListEqual(tax_component, ["_Test TDS"]) +class TestSalarySlipSafeEval(FrappeTestCase): + def test_safe_eval_for_salary_slip(self): + TEST_CASES = { + "1+1": 2, + '"abc" in "abl"': False, + '"a" in "abl"': True, + '"a" in ("a", "b")': True, + '"a" in {"a", "b"}': True, + '"a" in {"a": 1, "b": 2}': True, + '"a" in ["a" ,"b"]': True, + } + + for code, result in TEST_CASES.items(): + self.assertEqual(_safe_eval(code), result) + + self.assertRaises(NameError, _safe_eval, "frappe.utils.os.path", {}) + + # Doc/dict objects + user = frappe.new_doc("User") + user.user_type = "System User" + user.enabled = 1 + self.assertTrue(_safe_eval("user_type == 'System User'", eval_locals=user.as_dict())) + self.assertEqual( + "System User Test", _safe_eval("user_type + ' Test'", eval_locals=user.as_dict()) + ) + self.assertEqual(1, _safe_eval("int(enabled)", eval_locals=user.as_dict())) + + # Walrus not allowed + self.assertRaises(SyntaxError, _safe_eval, "(x := (40+2))") + + # Format check but saner + self.assertTrue(_safe_eval("'x' != 'Information Techonology'")) + self.assertRaises(SyntaxError, _safe_eval, "'blah'.format(1)") + + def make_income_tax_components(): tax_components = [ {