@@ -815,11 +815,47 @@ 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 (
847+ f"Unsupported operator: { type (node .op ).__name__ } " )
848+ if isinstance (node , ast .Call ):
849+ if not (isinstance (node .func , ast .Name )
850+ and node .func .id == 'cycler' ):
851+ raise ValueError (
852+ "only the 'cycler()' function is allowed" )
853+ args = [ast .literal_eval (ast .unparse (a )) for a in node .args ]
854+ kwargs = {kw .arg : ast .literal_eval (ast .unparse (kw .value ))
855+ for kw in node .keywords }
856+ return cycler (* args , ** kwargs )
857+ raise ValueError (
858+ f"Unsupported expression in cycler string: { ast .dump (node )} " )
823859
824860
825861# A validator dedicated to the named legend loc
@@ -870,25 +906,11 @@ def _validate_legend_loc(loc):
870906def validate_cycler (s ):
871907 """Return a Cycler object from a string repr or the object itself."""
872908 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()
884909 try :
885- _DunderChecker ().visit (ast .parse (s ))
886- s = eval (s , {'cycler' : cycler , '__builtins__' : {}})
887- except BaseException as e :
910+ s = _parse_cycler_string (s )
911+ except Exception as e :
888912 raise ValueError (f"{ s !r} is not a valid cycler construction: { e } "
889913 ) from e
890- # Should make sure what comes from the above eval()
891- # is a Cycler object.
892914 if isinstance (s , Cycler ):
893915 cycler_inst = s
894916 else :
0 commit comments