Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork8.3k
SEC: Remove eval() from validate_cycler#31248
SEC: Remove eval() from validate_cycler#31248scottshambaugh wants to merge 7 commits intomatplotlib:mainfrom
Conversation
…through a malicious matplotlibrc
timhoffm left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Please add some tests.
scottshambaugh commentedMar 7, 2026 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
This was pretty well exercised with |
| ValueError), | ||
| ("cycler('c', [j.\u000c__class__(j) for j in ['r', 'b']])", | ||
| ValueError), | ||
| ("cycler('c', [j.__class__(j).lower() for j in ['r', 'b']])", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
This seems to imply that since only dunder methods are blocked, one could call.lower(). Indeed, this does work currently:
>>>mpl.rcParams['axes.prop_cycle']='cycler("color", [x.lower() for x in "RGB"])'>>>mpl.rcParams['axes.prop_cycle']cycler('color', ['r','g','b'])
This doesn't appear on thesuccess list however, so I'm not sure it'sintended to work.
In this PR it now fails, with a bit of an inscrutable error:
ValueError: Key axes.prop_cycle: 'cycler("color", [x.lower() for x in "RGB"])' is not a valid cycler construction: malformed node or string on line 1: <ast.ListComp object at 0x7fb87f6074d0>There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
I think that’s ok. I consider the cycler spec in matplotlibrc as purely declarative. You should write out the lists explicitly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
non-dunder methods of literals were intended to work. You are right, I should have added that to the list. So, some people may have discovered that and used it.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
lib/matplotlib/rcsetup.py Outdated
| kwargs = {kw.arg: ast.literal_eval(kw.value) for kw in node.keywords} | ||
| return cycler(*args, **kwargs) | ||
| raise ValueError( | ||
| f"Unsupported expression in cycler string: {ast.dump(node)}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Please keep in mind all of the valid ways cyclers can be composed:https://matplotlib.org/cycler/#composition. We didn't test all of it here because it was tested in that project.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Thanks, I had missed that link.
It seems like the operations that were missing were integer multiplication, concat, and slicing. I believe all of those should be safe, so added them in here explicitly with some tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Arbitrary non-dunder methods on the other hand I don't think we should allow here given the security risk. I'm a little skeptical that their use would be widespread, but we could always add back in specific safe methods (e.g. lower()) if people complain.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
methods from standard types that derive from literals are "safe" in that they won't provide a mechanism to modify state or chain to arbitrary code execution. At least, that was the view we had when we originally designed this.
scottshambaughMar 9, 2026 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Had Claude tackle this for 20 minutes and found an exploit to execute arbitrary code with the original implementation using non-dunder methods. Can import modules, write to file, etc. I'll dm you on discourse instead of posting it publicly.
Edit: Don't see you on discourse@WeatherGod! Can email you if you'd like, if you reach out with contact info.
scottshambaughMar 9, 2026 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
I'm going to raise this with cpython core, since the__builtins__ approach is (incorrectly) advertised as limiting the scope:
https://docs.python.org/3/library/functions.html#eval
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Good idea to raise this with cpython devs because that would be a significant security issue. No need to inform me what the exploit is, I'd rather not have that knowledge. Just rather know if it can be fixed from cpython's end.
Given that you were able to find a possible exploit validates the effort here to move away from the old appraoch. I did something similar to this for an employer, so I need to go back over my notes and see if there is anything else to watch out for.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Found a good amount of prior art on this method since yesterday so it's not new... my ask to the cpython security team was to update the eval docs because "you can control what builtins are available to the executed code" is incorrect security guidance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
cpython docs PR:python/cpython#145773
Uh oh!
There was an error while loading.Please reload this page.
WeatherGod commentedMar 10, 2026
I don't disagree with limiting the parsing of the cycler parameter, but strictly speaking, it is an API change which normally would go through a deprecation cycle. I'm assuming that the parsing limitations goes beyond what is strictly needed to close any known security holes. What I'm worried about is that I don't know if we have any idea just how widespread the more advanced formulations of cycler is out there that could be impacted by this change. Is it at all possible to plug known security holes with the current approach and raise a deprecation warning if we detect the sort of stuff we want to eventually restrict? |
scottshambaugh commentedMar 10, 2026
Unfortunately, my takeaway from what I've been reading about this is that it's essentially impossible to sandbox See for example a myriad of techniques on display here:https://gist.github.com/rebane2001/a3734ecae4a05e3b85bafc1ae17ee864 |
scottshambaugh commentedMar 10, 2026
Added a deprecation notice. |
tacaswell commentedMar 10, 2026
I've also found some of the ways to escape the current validation. Based on what they look like if users are "legitimately" doing those things to get a cycler then I have no compunction about breaking them. |
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
| def _eval_cycler_expr(node): | ||
| """Recursively evaluate an AST node to build a Cycler object.""" | ||
| if isinstance(node, ast.BinOp): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
if I am reading this right we support the binary ops between any valid expressions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Yes, do you see any issues with that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
I don't see an issue, but just thinking through exactly how locked down we can make it. For example, can we check that at least one side of this is a Cycler?
| def _eval_cycler_expr(node): | ||
| """Recursively evaluate an AST node to build a Cycler object.""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Would it be better to utilizeast.walk() to walk the tree nodes? It should help prevent any recursion errors or other oddities that can happen with trees.
| and node.func.id in ('cycler', 'concat')): | ||
| raise ValueError( | ||
| "only the 'cycler()' and 'concat()' functions are allowed") | ||
| func = cycler if node.func.id == 'cycler' else cconcat |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
| func=cyclerifnode.func.id=='cycler'elsecconcat | |
| func=ccyclerifnode.func.id=='cycler'elsecconcat |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
The validation on each node withcycler is needed, tests fail withccycler.
Uh oh!
There was an error while loading.Please reload this page.
PR summary
validate_cycler()useseval()to parseaxes.prop_cyclercParam strings. Combined with automatic matplotlibrc loading from the current working directory, this allows arbitrary code execution onimport matplotlibvia a malicious config file in a local directory. This poses a security risk.AI Disclosure
Claude was used to run the audit. Code manually reviewed
PR checklist