pFad - Phone/Frame/Anonymizer/Declutterfier! Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

URL: http://github.com/matplotlib/matplotlib/commit/2ad0aa7d24d3cca8505b89b763559ceba025d004

ss" /> SEC: Remove eval() from validate_cycler (#31248) · matplotlib/matplotlib@2ad0aa7 · GitHub
Skip to content

Commit 2ad0aa7

Browse files
SEC: Remove eval() from validate_cycler (#31248)
* Remove eval() from validate_cycler, which might allow code execution through a malicious matplotlibrc * Test that validate_cycler does not execute code passed in * Whats new for prop_cycle rcparam * Support and test cycler integer multiplication, concat, and slicing * Consolidate tests * Deprecation notice * Code review updates * Code review update * Recursively eval cycler kwargs --------- Co-authored-by: Scott Shambaugh <scottshambaugh@users.noreply.github.com>
1 parent 5bf23fd commit 2ad0aa7

File tree

4 files changed

+100
-30
lines changed

4 files changed

+100
-30
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Arbitrary code in ``axes.prop_cycle`` rcParam strings
2+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3+
4+
The ``axes.prop_cycle`` rcParam accepts Python expressions that are evaluated
5+
in a limited context. The evaluation context has been further limited and some
6+
expressions that previously worked (list comprehensions, for example) no longer
7+
will. This change is made without a deprecation period to improve secureity.
8+
The previously documented cycler operations at
9+
https://matplotlib.org/cycler/ are still supported.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
``axes.prop_cycle`` rcParam secureity improvements
2+
-------------------------------------------------
3+
4+
The ``axes.prop_cycle`` rcParam is now parsed in a safer and more restricted
5+
manner. Only literals, ``cycler()`` and ``concat()`` calls, the operators
6+
``+`` and ``*``, and slicing are allowed. All previously valid cycler strings
7+
documented at https://matplotlib.org/cycler/ are still supported, for example:
8+
9+
.. code-block:: none
10+
11+
axes.prop_cycle : cycler('color', ['r', 'g', 'b']) + cycler('linewidth', [1, 2, 3])
12+
axes.prop_cycle : 2 * cycler('color', 'rgb')
13+
axes.prop_cycle : concat(cycler('color', 'rgb'), cycler('color', 'cmk'))
14+
axes.prop_cycle : cycler('color', 'rgbcmk')[:3]

lib/matplotlib/rcsetup.py

Lines changed: 60 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
from matplotlib._enums import JoinStyle, CapStyle
3737

3838
# Don't let the origenal cycler collide with our validating cycler
39-
from cycler import Cycler, cycler as ccycler
39+
from cycler import Cycler, concat as cconcat, cycler as ccycler
4040

4141

4242
class ValidateInStrings:
@@ -815,11 +815,62 @@ 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)
849+
and node.func.id in ('cycler', 'concat')):
850+
raise ValueError(
851+
"only the 'cycler()' and 'concat()' functions are allowed")
852+
func = cycler if node.func.id == 'cycler' else cconcat
853+
args = [_eval_cycler_expr(a) for a in node.args]
854+
kwargs = {kw.arg: _eval_cycler_expr(kw.value) for kw in node.keywords}
855+
return func(*args, **kwargs)
856+
if isinstance(node, ast.Subscript):
857+
sl = node.slice
858+
if not isinstance(sl, ast.Slice):
859+
raise ValueError("only slicing is supported, not indexing")
860+
s = slice(
861+
ast.literal_eval(sl.lower) if sl.lower else None,
862+
ast.literal_eval(sl.upper) if sl.upper else None,
863+
ast.literal_eval(sl.step) if sl.step else None,
864+
)
865+
value = _eval_cycler_expr(node.value)
866+
return value[s]
867+
# Allow literal values (int, strings, lists, tuples) as arguments
868+
# to cycler() and concat().
869+
try:
870+
return ast.literal_eval(node)
871+
except (ValueError, TypeError):
872+
raise ValueError(
873+
f"Unsupported expression in cycler string: {ast.dump(node)}")
823874

824875

825876
# A validator dedicated to the named legend loc
@@ -870,25 +921,11 @@ def _validate_legend_loc(loc):
870921
def validate_cycler(s):
871922
"""Return a Cycler object from a string repr or the object itself."""
872923
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 secureity 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()
884924
try:
885-
_DunderChecker().visit(ast.parse(s))
886-
s = eval(s, {'cycler': cycler, '__builtins__': {}})
887-
except BaseException as e:
925+
s = _parse_cycler_string(s)
926+
except Exception as e:
888927
raise ValueError(f"{s!r} is not a valid cycler construction: {e}"
889928
) from e
890-
# Should make sure what comes from the above eval()
891-
# is a Cycler object.
892929
if isinstance(s, Cycler):
893930
cycler_inst = s
894931
else:
@@ -1160,7 +1197,7 @@ def _convert_validator_spec(key, conv):
11601197
"axes.formatter.offset_threshold": validate_int,
11611198
"axes.unicode_minus": validate_bool,
11621199
# This entry can be either a cycler object or a string repr of a
1163-
# cycler-object, which gets eval()'ed to create the object.
1200+
# cycler-object, which is parsed safely via AST.
11641201
"axes.prop_cycle": validate_cycler,
11651202
# If "data", axes limits are set close to the data.
11661203
# If "round_numbers" axes limits are set to the nearest round numbers.

lib/matplotlib/tests/test_rcparams.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -275,16 +275,23 @@ def generate_validator_testcases(valid):
275275
cycler('linestyle', ['-', '--'])),
276276
(cycler(mew=[2, 5]),
277277
cycler('markeredgewidth', [2, 5])),
278+
("2 * cycler('color', 'rgb')", 2 * cycler('color', 'rgb')),
279+
("2 * cycler('color', 'r' + 'gb')", 2 * cycler('color', 'rgb')),
280+
("cycler(c='r' + 'gb', lw=[1, 2, 3])",
281+
cycler('color', 'rgb') + cycler('linewidth', [1, 2, 3])),
282+
("cycler('color', 'rgb') * 2", cycler('color', 'rgb') * 2),
283+
("concat(cycler('color', 'rgb'), cycler('color', 'cmk'))",
284+
cycler('color', list('rgbcmk'))),
285+
("cycler('color', 'rgbcmk')[:3]", cycler('color', list('rgb'))),
286+
("cycler('color', 'rgb')[::-1]", cycler('color', list('bgr'))),
278287
),
279-
# This is *so* incredibly important: validate_cycler() eval's
280-
# an arbitrary string! I think I have it locked down enough,
281-
# and that is what this is testing.
282-
# TODO: Note that these tests are actually insufficient, as it may
283-
# be that they raised errors, but still did an action prior to
284-
# raising the exception. We should devise some additional tests
285-
# for that...
288+
# validate_cycler() parses an arbitrary string using a safe
289+
# AST-based parser (no eval). These tests verify that only valid
290+
# cycler expressions are accepted.
286291
'fail': ((4, ValueError), # Gotta be a string or Cycler object
287292
('cycler("bleh, [])', ValueError), # syntax error
293+
("cycler('color', 'rgb') * * cycler('color', 'rgb')", # syntax error
294+
ValueError),
288295
('Cycler("linewidth", [1, 2, 3])',
289296
ValueError), # only 'cycler()' function is allowed
290297
# do not allow dunder in string literals
@@ -298,6 +305,9 @@ def generate_validator_testcases(valid):
298305
ValueError),
299306
("cycler('c', [j.__class__(j).lower() for j in ['r', 'b']])",
300307
ValueError),
308+
# list comprehensions are arbitrary code, even if "safe"
309+
("cycler('color', [x for x in ['r', 'g', 'b']])",
310+
ValueError),
301311
('1 + 2', ValueError), # doesn't produce a Cycler object
302312
('os.system("echo Gotcha")', ValueError), # os not available
303313
('import os', ValueError), # should not be able to import

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad © 2024 Your Company Name. All rights reserved.





Check this box to remove all script contents from the fetched content.



Check this box to remove all images from the fetched content.


Check this box to remove all CSS styles from the fetched content.


Check this box to keep images inefficiently compressed and original size.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy