Skip to content

Commit 45bb475

Browse files
authored
Merge commit from fork
Multiple security hardening fixes
2 parents b57e8c0 + 870b68e commit 45bb475

File tree

3 files changed

+95
-20
lines changed

3 files changed

+95
-20
lines changed

Diff for: asteval/asteval.py

+5-19
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@
4444
import time
4545
from sys import exc_info, stderr, stdout
4646

47-
from .astutils import (HAS_NUMPY, UNSAFE_ATTRS, UNSAFE_ATTRS_DTYPES,
47+
from .astutils import (HAS_NUMPY,
4848
ExceptionHolder, ReturnedNone, Empty, make_symbol_table,
49-
numpy, op2func, valid_symbol_name, Procedure)
49+
numpy, op2func, safe_getattr, safe_format, valid_symbol_name, Procedure)
5050

5151
ALL_NODES = ['arg', 'assert', 'assign', 'attribute', 'augassign', 'binop',
5252
'boolop', 'break', 'bytes', 'call', 'compare', 'constant',
@@ -513,7 +513,7 @@ def on_formattedvalue(self, node): # ('value', 'conversion', 'format_spec')
513513
fmt = '{__fstring__}'
514514
if node.format_spec is not None:
515515
fmt = f'{{__fstring__:{self.run(node.format_spec)}}}'
516-
return fmt.format(__fstring__=val)
516+
return safe_format(fmt, self.raise_exception, node, __fstring__=val)
517517

518518
def _getsym(self, node):
519519
val = self.symtable.get(node.id, ReturnedNone)
@@ -573,22 +573,8 @@ def on_attribute(self, node): # ('value', 'attr', 'ctx')
573573
sym = self.run(node.value)
574574
if ctx == ast.Del:
575575
return delattr(sym, node.attr)
576-
#
577-
unsafe = (node.attr in UNSAFE_ATTRS or
578-
(node.attr.startswith('__') and node.attr.endswith('__')))
579-
if not unsafe:
580-
for dtype, attrlist in UNSAFE_ATTRS_DTYPES.items():
581-
unsafe = isinstance(sym, dtype) and node.attr in attrlist
582-
if unsafe:
583-
break
584-
if unsafe:
585-
msg = f"no safe attribute '{node.attr}' for {repr(sym)}"
586-
self.raise_exception(node, exc=AttributeError, msg=msg)
587-
else:
588-
try:
589-
return getattr(sym, node.attr)
590-
except AttributeError:
591-
pass
576+
577+
return safe_getattr(sym, node.attr, self.raise_exception, node)
592578

593579

594580
def on_assign(self, node): # ('targets', 'value')

Diff for: asteval/astutils.py

+49-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from tokenize import ENCODING as tk_ENCODING
1414
from tokenize import NAME as tk_NAME
1515
from tokenize import tokenize as generate_tokens
16+
from string import Formatter
1617

1718
builtins = __builtins__
1819
if not isinstance(builtins, dict):
@@ -33,6 +34,14 @@
3334
except ImportError:
3435
pass
3536

37+
# This is a necessary API but it's undocumented and moved around
38+
# between Python releases
39+
try:
40+
from _string import formatter_field_name_split
41+
except ImportError:
42+
formatter_field_name_split = lambda \
43+
x: x._formatter_field_name_split()
44+
3645

3746

3847
MAX_EXPONENT = 10000
@@ -59,7 +68,7 @@
5968
'__getattribute__', '__subclasshook__', '__new__',
6069
'__init__', 'func_globals', 'func_code', 'func_closure',
6170
'im_class', 'im_func', 'im_self', 'gi_code', 'gi_frame',
62-
'f_locals', '__asteval__')
71+
'f_locals', '__asteval__','mro')
6372

6473
# unsafe attributes for particular objects, by type
6574
UNSAFE_ATTRS_DTYPES = {str: ('format', 'format_map')}
@@ -266,6 +275,45 @@ def safe_lshift(arg1, arg2):
266275
ast.UAdd: lambda a: +a,
267276
ast.USub: lambda a: -a}
268277

278+
# Safe version of getattr
279+
280+
def safe_getattr(obj, attr, raise_exc, node):
281+
"""safe version of getattr"""
282+
unsafe = (attr in UNSAFE_ATTRS or
283+
(attr.startswith('__') and attr.endswith('__')))
284+
if not unsafe:
285+
for dtype, attrlist in UNSAFE_ATTRS_DTYPES.items():
286+
unsafe = (isinstance(obj, dtype) or obj is dtype) and attr in attrlist
287+
if unsafe:
288+
break
289+
if unsafe:
290+
msg = f"no safe attribute '{attr}' for {repr(obj)}"
291+
raise_exc(node, exc=AttributeError, msg=msg)
292+
else:
293+
try:
294+
return getattr(obj, attr)
295+
except AttributeError:
296+
pass
297+
298+
class SafeFormatter(Formatter):
299+
def __init__(self, raise_exc, node):
300+
self.raise_exc = raise_exc
301+
self.node = node
302+
super().__init__()
303+
304+
def get_field(self, field_name, args, kwargs):
305+
first, rest = formatter_field_name_split(field_name)
306+
obj = self.get_value(first, args, kwargs)
307+
for is_attr, i in rest:
308+
if is_attr:
309+
obj = safe_getattr(obj, i, self.raise_exc, self.node)
310+
else:
311+
obj = obj[i]
312+
return obj, first
313+
314+
def safe_format(_string, raise_exc, node, *args, **kwargs):
315+
formatter = SafeFormatter(raise_exc, node)
316+
return formatter.vformat(_string, args, kwargs)
269317

270318
def valid_symbol_name(name):
271319
"""Determine whether the input symbol name is a valid name.

Diff for: tests/test_asteval.py

+41
Original file line numberDiff line numberDiff line change
@@ -1586,6 +1586,47 @@ def my_func(x, y):
15861586
etype, fullmsg = error.get_error()
15871587
assert 'no safe attribute' in error.msg
15881588
assert etype == 'AttributeError'
1589+
1590+
@pytest.mark.parametrize("nested", [False, True])
1591+
def test_unsafe_format_string_access(nested):
1592+
"""
1593+
addressing https://github.com/lmfit/asteval/security/advisories/GHSA-3wwr-3g9f-9gc7
1594+
"""
1595+
interp = make_interpreter(nested_symtable=nested)
1596+
interp(textwrap.dedent("""
1597+
f"{dict:'\\x7B__fstring__.__class__.s\\x7D'}"
1598+
1599+
"""), raise_errors=False)
1600+
1601+
error = interp.error[0]
1602+
etype, fullmsg = error.get_error()
1603+
assert 'no safe attribute' in error.msg
1604+
assert etype == 'AttributeError'
1605+
1606+
@pytest.mark.parametrize("nested", [False, True])
1607+
def test_unsafe_attr_dtypes(nested):
1608+
"""
1609+
addressing https://github.com/lmfit/asteval/security/advisories/GHSA-3wwr-3g9f-9gc7
1610+
"""
1611+
interp = make_interpreter(nested_symtable=nested)
1612+
interp(textwrap.dedent("""
1613+
'{0}'.format(dict)
1614+
"""), raise_errors=False)
1615+
1616+
error = interp.error[0]
1617+
etype, fullmsg = error.get_error()
1618+
assert 'no safe attribute' in error.msg
1619+
assert etype == 'AttributeError'
1620+
1621+
interp = make_interpreter(nested_symtable=nested)
1622+
interp(textwrap.dedent("""
1623+
str.format('{0}', dict)
1624+
"""), raise_errors=False)
1625+
1626+
error = interp.error[0]
1627+
etype, fullmsg = error.get_error()
1628+
assert 'no safe attribute' in error.msg
1629+
assert etype == 'AttributeError'
15891630

15901631

15911632
@pytest.mark.parametrize("nested", [False, True])

0 commit comments

Comments
 (0)