FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

SEC: Remove eval() from validate_cycler by scottshambaugh · Pull Request #31248 · matplotlib/matplotlib · GitHub

SEC: Remove eval() from validate_cycler - #31248

Merged
tacaswell merged 9 commits into
matplotlib:mainfrom
scottshambaugh:rcparam_eval
Mar 27, 2026
Merged

SEC: Remove eval() from validate_cycler#31248
tacaswell merged 9 commits into
matplotlib:mainfrom
scottshambaugh:rcparam_eval

Conversation

scottshambaugh commented Mar 6, 2026
edited
Loading

Copy link
Copy Markdown
Contributor

PR summary

validate_cycler() uses eval() to parse axes.prop_cycle rcParam strings. Combined with automatic matplotlibrc loading from the current working directory, this allows arbitrary code execution on import matplotlib via 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

scottshambaugh added this to the v3.11.0 milestone Mar 6, 2026
scottshambaugh added the Release critical For bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions. label Mar 6, 2026

timhoffm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Please add some tests.

scottshambaugh commented Mar 7, 2026
edited
Loading

Copy link
Copy Markdown
Contributor Author

This was pretty well exercised with test_validator_invalid already. I added test_validate_cycler_no_code_execution with a (benign) proof of concept code injection that evaluates on main but not this branch.

ValueError),
("cycler('c', [j.\u000c__class__(j) for j in ['r', 'b']])",
ValueError),
("cycler('c', [j.__class__(j).lower() for j in ['r', 'b']])",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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 the success list however, so I'm not sure it's intended 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>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

I think that’s ok. I consider the cycler spec in matplotlibrc as purely declarative. You should write out the lists explicitly.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Lets not support lower until we get a bug report.

Comment thread lib/matplotlib/tests/test_rcparams.py Outdated
Comment thread lib/matplotlib/rcsetup.py Outdated
Comment thread lib/matplotlib/rcsetup.py
Comment thread 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)}")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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.

scottshambaugh Mar 9, 2026
edited
Loading

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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.

scottshambaugh Mar 9, 2026
edited
Loading

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

cpython docs PR: python/cpython#145773

scottshambaugh Mar 11, 2026
edited
Loading

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Comment thread lib/matplotlib/tests/test_rcparams.py Outdated

Copy link
Copy Markdown
Member

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?

Copy link
Copy Markdown
Contributor Author

Unfortunately, my takeaway from what I've been reading about this is that it's essentially impossible to sandbox eval() by blocking lists of known exploits, and an allowlist of operations with ast.literal_eval is the only reasonably safe way of allowing users to submit arbitrary code.

See for example a myriad of techniques on display here: https://gist.github.com/rebane2001/a3734ecae4a05e3b85bafc1ae17ee864

Copy link
Copy Markdown
Contributor Author

Added a deprecation notice.

Copy link
Copy Markdown
Member

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.

Comment thread lib/matplotlib/rcsetup.py
Comment thread lib/matplotlib/rcsetup.py
Comment thread lib/matplotlib/rcsetup.py
scottshambaugh added the Security Hardening Proactive security hardening. Existing vulnerabilities should be reported per our security policy label Mar 12, 2026

WeatherGod commented Mar 14, 2026 via email

Copy link
Copy Markdown
Member

Comment thread lib/matplotlib/rcsetup.py Outdated
Comment thread lib/matplotlib/rcsetup.py Outdated

Copy link
Copy Markdown
Member

I'm ok with this going in as-is, but both of Elliott's comments are valid.

timhoffm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Should we backport? / Will there be a 3.10.9 release?

QuLogic commented Mar 26, 2026
edited
Loading

Copy link
Copy Markdown
Member

You will need to rebase if you want to get docs build working due to differences in how it processes PR merges/heads.

QuLogic commented Mar 26, 2026

Copy link
Copy Markdown
Member

But on the other hand CI is broken without setuptools-scm being pinned, so you may want to wait on that.

tacaswell merged commit 2ad0aa7 into matplotlib:main Mar 27, 2026
54 of 67 checks passed

ksunden commented Apr 17, 2026

Copy link
Copy Markdown
Member

@meeseeksdev please backport to v3.10.x

lumberbot-app Bot commented Apr 17, 2026

Copy link
Copy Markdown

Owee, I'm MrMeeseeks, Look at me.

There seem to be a conflict, please backport manually. Here are approximate instructions:

  1. Checkout backport branch and update it.
git checkout v3.10.x
git pull
  1. Cherry pick the first parent branch of the this PR on top of the older branch:
git cherry-pick -x -m1 2ad0aa7d24d3cca8505b89b763559ceba025d004
  1. You will likely have some merge/cherry-pick conflict here, fix them and commit:
git commit -am 'Backport PR #31248: SEC: Remove eval() from validate_cycler'
  1. Push to a named branch:
git push YOURFORK v3.10.x:auto-backport-of-pr-31248-on-v3.10.x
  1. Create a PR against branch v3.10.x, I would have named this PR:

"Backport PR #31248 on branch v3.10.x (SEC: Remove eval() from validate_cycler)"

And apply the correct labels and milestones.

Congratulations — you did some good work! Hopefully your backport PR will be tested by the continuous integration and merged soon!

Remember to remove the Still Needs Manual Backport label once the PR gets merged.

If these instructions are inaccurate, feel free to suggest an improvement.

ksunden pushed a commit to ksunden/matplotlib that referenced this pull request Apr 21, 2026
ksunden pushed a commit to ksunden/matplotlib that referenced this pull request Apr 21, 2026
timhoffm added a commit that referenced this pull request Apr 22, 2026
Backport PR #31248: SEC: Remove eval() from validate_cycler
ksunden mentioned this pull request Apr 24, 2026
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Release critical For bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions. Security Hardening Proactive security hardening. Existing vulnerabilities should be reported per our security policy topic: rcparams

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants


Back | FazBrowse Home | New Git URL