Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 09b61ac

Browse files
Remove eval() from validate_cycler, which might allow code execution through a malicious matplotlibrc
1 parent d3f5e11 commit 09b61ac

1 file changed

Lines changed: 40 additions & 21 deletions

File tree

lib/matplotlib/rcsetup.py

Lines changed: 40 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -815,11 +815,44 @@ def cycler(*args, **kwargs):
815815
return reduce(operator.add, (ccycler(k, v) for k, v in validated))
816816

817817

818-
class _DunderChecker(ast.NodeVisitor):
819-
def visit_Attribute(self, node):
820-
if node.attr.startswith("__") and node.attr.endswith("__"):
821-
raise ValueError("cycler strings with dunders are forbidden")
822-
self.generic_visit(node)
818+
def _parse_cycler_string(s):
819+
"""
820+
Parse a string representation of a cycler into a Cycler object safely,
821+
without using eval().
822+
823+
Accepts expressions like::
824+
825+
cycler('color', ['r', 'g', 'b'])
826+
cycler('color', 'rgb') + cycler('linewidth', [1, 2, 3])
827+
cycler(c='rgb', lw=[1, 2, 3])
828+
cycler('c', 'rgb') * cycler('linestyle', ['-', '--'])
829+
"""
830+
try:
831+
tree = ast.parse(s, mode='eval')
832+
except SyntaxError as e:
833+
raise ValueError(f"Could not parse {s!r}: {e}") from e
834+
return _eval_cycler_expr(tree.body)
835+
836+
837+
def _eval_cycler_expr(node):
838+
"""Recursively evaluate an AST node to build a Cycler object."""
839+
if isinstance(node, ast.BinOp):
840+
left = _eval_cycler_expr(node.left)
841+
right = _eval_cycler_expr(node.right)
842+
if isinstance(node.op, ast.Add):
843+
return left + right
844+
if isinstance(node.op, ast.Mult):
845+
return left * right
846+
raise ValueError(f"Unsupported operator: {type(node.op).__name__}")
847+
if isinstance(node, ast.Call):
848+
if not (isinstance(node.func, ast.Name) and node.func.id == 'cycler'):
849+
raise ValueError("only the 'cycler()' function is allowed")
850+
args = [ast.literal_eval(ast.unparse(a)) for a in node.args]
851+
kwargs = {kw.arg: ast.literal_eval(ast.unparse(kw.value))
852+
for kw in node.keywords}
853+
return cycler(*args, **kwargs)
854+
raise ValueError(
855+
f"Unsupported expression in cycler string: {ast.dump(node)}")
823856

824857

825858
# A validator dedicated to the named legend loc
@@ -870,25 +903,11 @@ def _validate_legend_loc(loc):
870903
def validate_cycler(s):
871904
"""Return a Cycler object from a string repr or the object itself."""
872905
if isinstance(s, str):
873-
# TODO: We might want to rethink this...
874-
# While I think I have it quite locked down, it is execution of
875-
# arbitrary code without sanitation.
876-
# Combine this with the possibility that rcparams might come from the
877-
# internet (future plans), this could be downright dangerous.
878-
# I locked it down by only having the 'cycler()' function available.
879-
# UPDATE: Partly plugging a security hole.
880-
# I really should have read this:
881-
# https://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html
882-
# We should replace this eval with a combo of PyParsing and
883-
# ast.literal_eval()
884906
try:
885-
_DunderChecker().visit(ast.parse(s))
886-
s = eval(s, {'cycler': cycler, '__builtins__': {}})
887-
except BaseException as e:
907+
s = _parse_cycler_string(s)
908+
except Exception as e:
888909
raise ValueError(f"{s!r} is not a valid cycler construction: {e}"
889910
) from e
890-
# Should make sure what comes from the above eval()
891-
# is a Cycler object.
892911
if isinstance(s, Cycler):
893912
cycler_inst = s
894913
else:

0 commit comments

Comments
 (0)