Skip to content

SEC: Remove eval() from validate_cycler#31248

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

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

Conversation

@scottshambaugh
Copy link
Copy Markdown
Contributor

@scottshambaugh scottshambaugh commented Mar 6, 2026

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 scottshambaugh added this to the v3.11.0 milestone Mar 6, 2026
@scottshambaugh scottshambaugh added the Release critical For bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions. label Mar 6, 2026
Copy link
Copy Markdown
Member

@timhoffm timhoffm left a 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
Copy link
Copy Markdown
Contributor Author

scottshambaugh commented Mar 7, 2026

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.

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.

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.

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.

Lets not support lower until we get a bug report.

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.

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.

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.

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.

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.

Copy link
Copy Markdown
Contributor Author

@scottshambaugh scottshambaugh Mar 9, 2026

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.

Copy link
Copy Markdown
Contributor Author

@scottshambaugh scottshambaugh Mar 9, 2026

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

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.

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.

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.

cpython docs PR: python/cpython#145773

Copy link
Copy Markdown
Contributor Author

@scottshambaugh scottshambaugh Mar 11, 2026

Choose a reason for hiding this comment

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

@WeatherGod
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?

@scottshambaugh
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

@scottshambaugh
Copy link
Copy Markdown
Contributor Author

Added a deprecation notice.

@tacaswell
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.

@scottshambaugh scottshambaugh added the Security Hardening Proactive security hardening. Existing vulnerabilities should be reported per our security policy label Mar 12, 2026
@WeatherGod
Copy link
Copy Markdown
Member

WeatherGod commented Mar 14, 2026 via email

@tacaswell
Copy link
Copy Markdown
Member

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

Copy link
Copy Markdown
Member

@timhoffm timhoffm left a comment

Choose a reason for hiding this comment

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

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

@QuLogic
Copy link
Copy Markdown
Member

QuLogic commented Mar 26, 2026

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

@QuLogic
Copy link
Copy Markdown
Member

QuLogic commented Mar 26, 2026

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

@tacaswell tacaswell merged commit 2ad0aa7 into matplotlib:main Mar 27, 2026
54 of 67 checks passed
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.

6 participants