Skip to content

Commit

Permalink
Merge pull request #136 from lmfit/fix_unsafe_procedures
Browse files Browse the repository at this point in the history
Fix unsafe procedures
  • Loading branch information
newville authored Jan 14, 2025
2 parents aeea1b7 + 0508431 commit e4c87f6
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 57 deletions.
92 changes: 46 additions & 46 deletions asteval/astutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@
'insert', 'int32', 'integer', 'interp', 'intersect1d', 'invert',
'iscomplex', 'iscomplexobj', 'isfinite', 'isinf', 'isnan', 'isneginf',
'isposinf', 'isreal', 'isrealobj', 'isscalar', 'iterable', 'kaiser',
'kron', 'ldexp', 'left_shift', 'less', 'less_equal', 'linalg', 'linspace',
'kron', 'ldexp', 'left_shift', 'less', 'less_equal', 'linspace',
'little_endian', 'loadtxt', 'log', 'log10', 'log1p', 'log2', 'logaddexp',
'logaddexp2', 'logical_and', 'logical_not', 'logical_or', 'logical_xor',
'logspace', 'longdouble', 'longlong', 'mask_indices', 'matrix', 'maximum',
Expand Down Expand Up @@ -506,59 +506,59 @@ def __init__(self, name, interp, doc=None, lineno=0,
self.name = name
self.__name__ = self.name
self.__asteval__ = interp
self.__raise_exc__ = self.__asteval__.raise_exception
self.raise_exc = self.__asteval__.raise_exception
self.__doc__ = doc
self.__body__ = body
self.__argnames__ = args
self.__kwargs__ = kwargs
self.__vararg__ = vararg
self.__varkws__ = varkws
self.body = body
self.argnames = args
self.kwargs = kwargs
self.vararg = vararg
self.varkws = varkws
self.lineno = lineno
self.__ininit__ = False

def __setattr__(self, attr, val):
if not getattr(self, '__ininit__', True):
self.__raise_exc__(None, exc=TypeError,
self.raise_exc(None, exc=TypeError,
msg="procedure is read-only")
self.__dict__[attr] = val

def __dir__(self):
return ['__getdoc__', '__argnames__', 'kwargs', 'name', 'vararg', 'varkws']
return ['_getdoc', 'argnames', 'kwargs', 'name', 'vararg', 'varkws']

def __getdoc__(self):
def _getdoc(self):
doc = self.__doc__
if isinstance(doc, ast.Constant):
doc = doc.value
return doc

def __repr__(self):
"""Procedure repr"""
sig = self.__signature__()
"""TODO: docstring in magic method."""
sig = self._signature()
rep = f"<Procedure {sig}>"
doc = self.__getdoc__()
doc = self._getdoc()
if doc is not None:
rep = f"{rep}\n {doc}"
return rep

def __signature__(self):
"return the procedure's call signature"
def _signature(self):
"call signature"
sig = ""
if len(self.__argnames__) > 0:
sig = sig + ', '.join(self.__argnames__)
if self.__vararg__ is not None:
sig = sig + f"*{self.__vararg__}"
if len(self.__kwargs__) > 0:
if len(self.argnames) > 0:
sig = sig + ', '.join(self.argnames)
if self.vararg is not None:
sig = sig + f"*{self.vararg}"
if len(self.kwargs) > 0:
if len(sig) > 0:
sig = f"{sig}, "
_kw = [f"{k}={v}" for k, v in self.__kwargs__]
_kw = [f"{k}={v}" for k, v in self.kwargs]
sig = f"{sig}{', '.join(_kw)}"

if self.__varkws__ is not None:
sig = f"{sig}, **{self.__varkws__}"
if self.varkws is not None:
sig = f"{sig}, **{self.varkws}"
return f"{self.name}({sig})"

def __call__(self, *args, **kwargs):
"""call the Procedure"""
"""TODO: docstring in public method."""
topsym = self.__asteval__.symtable
if self.__asteval__.config.get('nested_symtable', False):
sargs = {'_main': topsym}
Expand All @@ -576,72 +576,72 @@ def __call__(self, *args, **kwargs):
args = list(args)
nargs = len(args)
nkws = len(kwargs)
nargs_expected = len(self.__argnames__)
nargs_expected = len(self.argnames)

# check for too few arguments, but the correct keyword given
if (nargs < nargs_expected) and nkws > 0:
for name in self.__argnames__[nargs:]:
for name in self.argnames[nargs:]:
if name in kwargs:
args.append(kwargs.pop(name))
nargs = len(args)
nargs_expected = len(self.__argnames__)
nargs_expected = len(self.argnames)
nkws = len(kwargs)
if nargs < nargs_expected:
msg = f"{self.name}() takes at least"
msg = f"{msg} {nargs_expected} arguments, got {nargs}"
self.__raise_exc__(None, exc=TypeError, msg=msg)
self.raise_exc(None, exc=TypeError, msg=msg)
# check for multiple values for named argument
if len(self.__argnames__) > 0 and kwargs is not None:
if len(self.argnames) > 0 and kwargs is not None:
msg = "multiple values for keyword argument"
for targ in self.__argnames__:
for targ in self.argnames:
if targ in kwargs:
msg = f"{msg} '{targ}' in Procedure {self.name}"
self.__raise_exc__(None, exc=TypeError, msg=msg, lineno=self.lineno)
self.raise_exc(None, exc=TypeError, msg=msg, lineno=self.lineno)

# check more args given than expected, varargs not given
if nargs != nargs_expected:
msg = None
if nargs < nargs_expected:
msg = f"not enough arguments for Procedure {self.name}()"
msg = f"{msg} (expected {nargs_expected}, got {nargs}"
self.__raise_exc__(None, exc=TypeError, msg=msg)
self.raise_exc(None, exc=TypeError, msg=msg)

if nargs > nargs_expected and self.__vararg__ is None:
if nargs - nargs_expected > len(self.__kwargs__):
if nargs > nargs_expected and self.vararg is None:
if nargs - nargs_expected > len(self.kwargs):
msg = f"too many arguments for {self.name}() expected at most"
msg = f"{msg} {len(self.__kwargs__)+nargs_expected}, got {nargs}"
self.__raise_exc__(None, exc=TypeError, msg=msg)
msg = f"{msg} {len(self.kwargs)+nargs_expected}, got {nargs}"
self.raise_exc(None, exc=TypeError, msg=msg)

for i, xarg in enumerate(args[nargs_expected:]):
kw_name = self.__kwargs__[i][0]
kw_name = self.kwargs[i][0]
if kw_name not in kwargs:
kwargs[kw_name] = xarg

for argname in self.__argnames__:
for argname in self.argnames:
symlocals[argname] = args.pop(0)

try:
if self.__vararg__ is not None:
symlocals[self.__vararg__] = tuple(args)
if self.vararg is not None:
symlocals[self.vararg] = tuple(args)

for key, val in self.__kwargs__:
for key, val in self.kwargs:
if key in kwargs:
val = kwargs.pop(key)
symlocals[key] = val

if self.__varkws__ is not None:
symlocals[self.__varkws__] = kwargs
if self.varkws is not None:
symlocals[self.varkws] = kwargs

elif len(kwargs) > 0:
msg = f"extra keyword arguments for Procedure {self.name}: "
msg = msg + ','.join(list(kwargs.keys()))
self.__raise_exc__(None, msg=msg, exc=TypeError,
self.raise_exc(None, msg=msg, exc=TypeError,
lineno=self.lineno)

except (ValueError, LookupError, TypeError,
NameError, AttributeError):
msg = f"incorrect arguments for Procedure {self.name}"
self.__raise_exc__(None, msg=msg, lineno=self.lineno)
self.raise_exc(None, msg=msg, lineno=self.lineno)

if self.__asteval__.config.get('nested_symtable', False):
save_symtable = self.__asteval__.symtable
Expand All @@ -655,7 +655,7 @@ def __call__(self, *args, **kwargs):
retval = None

# evaluate script of function
for node in self.__body__:
for node in self.body:
self.__asteval__.run(node, expr='<>', lineno=self.lineno)
if len(self.__asteval__.error) > 0:
break
Expand Down
13 changes: 7 additions & 6 deletions doc/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,11 @@
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build

INSTALLDIR = /home/newville/public_html/asteval/


# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(SPHINXOPTS) .

.PHONY: html pdf all help clean dirhtml latex

Expand All @@ -31,6 +26,12 @@ html:
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."

tarball: all
cd $(BUILDDIR)/html && tar cvzf ../../../_doc.tgz *
cd ../../
@echo


pdf: latex
cd $(BUILDDIR)/latex && make all-pdf
cp -pr $(BUILDDIR)/latex/asteval.pdf ./asteval.pdf
Expand Down
2 changes: 1 addition & 1 deletion doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@
# -- Options for LaTeX output --------------------------------------------------

# The paper size ('letter' or 'a4').
#latex_paper_size = 'letter'
latex_papersize = 'letter'

# The font size ('10pt', '11pt' or '12pt').
#latex_font_size = '10pt'
Expand Down
14 changes: 10 additions & 4 deletions doc/motivation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,16 @@ needed, these modules can be added to any Interpreter either using the
``user_symbols`` argument when creating it, or adding the needed symbols to the
symbol table after the Interpreter is created.

There are important categories of safety that asteval may attempt to address,
but cannot guarantee success. The most important of these is resource hogging,
which might be used for a denial-of-service attack. There is no guaranteed
timeout on any calculation, and so a reasonable looking calculation such as::
In 2025, a security audit by William Khem Marquez showed a
vulnerability from leaving some AST objects exposed within the
interpreter for user-defined functions ("Procedures"), and this was
fixed for version 1.0.6.

There are other categories of safety that asteval may attempt to
address, but cannot guarantee success. The most important of these is
resource hogging, which might be used for a denial-of-service attack.
There is no guaranteed timeout on any calculation, and so a reasonable
looking calculation such as::

from asteval import Interpreter
aeval = Interpreter()
Expand Down

0 comments on commit e4c87f6

Please sign in to comment.