From 9fda0b5033f58fe4352ee915801be81e019f22f0 Mon Sep 17 00:00:00 2001 From: Kaif Khan Date: Fri, 3 Jul 2026 10:33:34 +0530 Subject: [PATCH] cap %-format field widths in safe_mod --- asteval/astutils.py | 34 +++++++++++++++++++++++++++++++++- tests/test_asteval.py | 8 ++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/asteval/astutils.py b/asteval/astutils.py index a6defad..019b9f5 100644 --- a/asteval/astutils.py +++ b/asteval/astutils.py @@ -249,6 +249,38 @@ def safe_lshift(arg1, arg2): return arg1 << arg2 +# printf-style conversion specifier, used to bound %-formatting field sizes +_PERCENT_CONV = re.compile(r'%(?:\((?P[^)]*)\))?' + r'(?P[#0\- +]*)' + r'(?P\*|\d+)?' + r'(?:\.(?P\*|\d+))?' + r'[hlL]?' + r'(?P[diouxXeEfFgGcrsa%])') + + +def safe_mod(arg1, arg2): + """safe version of %, capping string %-formatting field sizes""" + if isinstance(arg1, str): + args = arg2 if isinstance(arg2, tuple) else (arg2,) + pos = 0 + for match in _PERCENT_CONV.finditer(arg1): + if match.group('type') == '%': + continue + for field in (match.group('width'), match.group('prec')): + if field == '*': + value = args[pos] if pos < len(args) else None + pos += 1 + elif field is not None: + value = int(field) + else: + value = None + if isinstance(value, int) and value > MAX_STR_LEN: + raise RuntimeError(f"String length exceeded, max string length is {MAX_STR_LEN}") + if match.group('key') is None: + pos += 1 + return arg1 % arg2 + + OPERATORS = {ast.Is: lambda a, b: a is b, ast.IsNot: lambda a, b: a is not b, ast.In: lambda a, b: a in b, @@ -265,7 +297,7 @@ def safe_lshift(arg1, arg2): ast.Pow: safe_pow, ast.MatMult: lambda a, b: a @ b, ast.Sub: lambda a, b: a - b, - ast.Mod: lambda a, b: a % b, + ast.Mod: safe_mod, ast.And: lambda a, b: a and b, ast.Or: lambda a, b: a or b, ast.Eq: lambda a, b: a == b, diff --git a/tests/test_asteval.py b/tests/test_asteval.py index f00be5b..b530da1 100644 --- a/tests/test_asteval.py +++ b/tests/test_asteval.py @@ -1234,6 +1234,14 @@ def test_safe_funcs(nested): check_error(interp, None) interp("1<<1001") check_error(interp, 'RuntimeError') + interp("'%5d' % 3") + check_error(interp, None) + interp("'%2000000d' % 0") + check_error(interp, 'RuntimeError') + interp("'%.2000000f' % 0.0") + check_error(interp, 'RuntimeError') + interp("'%*d' % (2000000, 0)") + check_error(interp, 'RuntimeError') @pytest.mark.parametrize("nested", [False, True]) def test_safe__numpyfuncs(nested):