Skip to content

[Sec]: Disable auto-loading custom matplotlibrc files #31346

Open

Description

@scottshambaugh

Issue Description:

matplotlibrc files are autoloaded on import matplotlib. When matplotlib_fname searches for this file, the first candidate it yields is the bare filename 'matplotlibrc' in the current working directory. If an attacker can place a matplotlibrc file in a directory where a user runs Python (e.g., a cloned git repository, a shared project directory, a downloads folder), all rcParams in that file are applied automatically when import matplotlib or import matplotlib.pyplot executes.

Several of the rcparams can set up common calls such as figure creation (#31293) or latex text rendering (#31249) such that they can execute arbitrary code from the matplotlibrc file as a side-effect. This presents a potential security risk.

The two linked threads above talk a bit about how an attacker which can place a file on the PATH, could alternatively just place an executable which wraps some module and silently imports with arbitrary import-time side effects, and so this is a security boundary that matplotlib should not try to include. My take is that this sort of script would appear as code and be likely flagged in a manual or automatic security audit, whereas a matplotlibrc file is not directly executable and appears as data. Someone who assumes the common principle of separating data and code would not think that this file might be risky.

This is not a critical vulnerability, but represents an opportunity for security hardening.

How to Solve:
Stop auto-loading custom matplotlibrc files. Allow the user to opt-in to this via a function or setting the MATPLOTLIBRC environment variable.

Note that this may require some discussion to figure out the best solution, and I do not recommend this as a first-time contributor issue.

Activity

  1. added
    Security HardeningProactive security hardening. Existing vulnerabilities should be reported per our security policy
    on Mar 21, 2026
  2. kx7m2qd commented on May 4, 2026

    @kx7m2qd

    Proposed approach: Drop CWD from the default search path
    The minimal-breaking-change fix here seems to be removing 'matplotlibrc' (bare CWD path) from the candidates yielded by matplotlib_fname(), while keeping ~/.config/matplotlib/matplotlibrc and the install-dir default intact.
    Users who currently rely on a per-project matplotlibrc in CWD could migrate to either:

    Setting MATPLOTLIBRC=./matplotlibrc explicitly
    Calling matplotlib.rc_file('./matplotlibrc') at the top of their script

    This mirrors how Python itself hardened sys.path by removing the implicit '' (CWD) entry — same class of attack, same fix pattern.
    Questions before opening a PR:

    Should this be a hard removal, or a deprecation warning cycle first (e.g. warn for one release, remove in the next)?
    Is there appetite for a MATPLOTLIBRC_UNSAFE_CWD=1 escape hatch for users who truly need the old behavior?

    Happy to open a PR implementing the CWD removal with a deprecation warning path if the approach looks reasonable to maintainers.

  3. rcomer commented on May 4, 2026

    @rcomer
    Member

    This mirrors how Python itself hardened sys.path by removing the implicit '' (CWD) entry

    Please could you provide a reference for this change?

  4. kx7m2qd commented on May 4, 2026

    @kx7m2qd

    The reference I had in mind is PEP 687 (implemented in Python 3.11), which introduced the -P flag and PYTHONSAFEPATH environment variable to suppress prepending '' and other potentially unsafe paths to sys.path.

    I should clarify my earlier phrasing Python didn't do a hard removal of '' from sys.path by default, but rather added an opt-in mechanism to disable the behavior. That's actually more analogous to what I was suggesting here: keep the old behavior available (via MATPLOTLIBRC env var or explicitrc_file()call), but stop doing it silently by default.

  5. tacaswell commented on May 4, 2026

    @tacaswell
    Member

    I think an env MATPLOTLIBNORC or MATPLOTLIBDISABLERC (I do not have a strong opinion on the exact name) which if set disables any automatic loading of matplotlibrc files is the best path here. It gives people/organizations that have a more restrictive posture a knob to turn, keeps the convenience for a vast majority of users, and does not require any deprecation cycle or change for anyone who is not already worried about this.

  6. added this to the v3.12.0 milestone on May 4, 2026
  7. kx7m2qd commented on May 4, 2026

    @kx7m2qd

    That makes sense an opt-in env var avoids any breaking change or deprecation cycle entirely, which is the right tradeoff for a library this widely used.

    Happy to open a PR implementing MATPLOTLIBNORC (or whichever name you settle on). Would check for it in matplotlib_fname() before yielding any candidates, so the entire auto-load path is skipped. I'd also add a note to the docs/migration guide so security-focused deployments can discover it.

    Let me know if you have a name preference and I'll get started.

  8. timhoffm commented on May 5, 2026

    @timhoffm
    Member

    From https://matplotlib.org/stable/users/prev_whats_new/whats_new_3.1.0.html#the-matplotlibrc-environment-variable-can-now-point-to-any-file-path

    The MATPLOTLIBRC environment variable can now point to any "file" path

    This includes device files; in particular, on Unix systems, one can set MATPLOTLIBRC to /dev/null to ignore the user's matplotlibrc file and fall back to Matplotlib's defaults.

    This means we already have an opt-out mechanism (or opt-in for disabling) via MATLPLOTLIBRC=/dev/null

    However, this is not portable. I suggest

  9. tacaswell commented on May 5, 2026

    @tacaswell
    Member

    def matplotlib_fname():
    """
    Get the location of the config file.
    The file location is determined in the following order
    - ``$PWD/matplotlibrc``
    - ``$MATPLOTLIBRC`` if it is not a directory
    - ``$MATPLOTLIBRC/matplotlibrc``
    - ``$MPLCONFIGDIR/matplotlibrc``
    - On Linux,
    - ``$XDG_CONFIG_HOME/matplotlib/matplotlibrc`` (if ``$XDG_CONFIG_HOME``
    is defined)
    - or ``$HOME/.config/matplotlib/matplotlibrc`` (if ``$XDG_CONFIG_HOME``
    is not defined)
    - On other platforms,
    - ``$HOME/.matplotlib/matplotlibrc`` if ``$HOME`` is defined
    - Lastly, it looks in ``$MATPLOTLIBDATA/matplotlibrc``, which should always
    exist.
    """
    def gen_candidates():
    # rely on down-stream code to make absolute. This protects us
    # from having to directly get the current working directory
    # which can fail if the user has ended up with a cwd that is
    # non-existent.
    yield 'matplotlibrc'
    try:
    matplotlibrc = os.environ['MATPLOTLIBRC']
    except KeyError:
    pass
    else:
    yield matplotlibrc
    yield os.path.join(matplotlibrc, 'matplotlibrc')
    yield os.path.join(get_configdir(), 'matplotlibrc')
    yield os.path.join(get_data_path(), 'matplotlibrc')
    for fname in gen_candidates():
    if os.path.exists(fname) and not os.path.isdir(fname):
    return fname
    raise RuntimeError("Could not find matplotlibrc file; your Matplotlib "
    "install is broken")

    Setting /dev/null will skip looking for the users matplotlib rc in any of the places it might be stashed, but we will still look in the CWD first. A null value disabling search vs other wrong values (e.g. a non-existent path) being silently skipped seems to be shoving too much into one ENV.

    The last thing in the fallback list is a custom matplotlibrc that a packager might have shipped in our data directory. Do we want to still read that or really go with no matpltolibrc reading at al?

  10. kx7m2qd commented on May 5, 2026

    @kx7m2qd

    Hey, appreciate the writeup and willingness to implement this!

    Looking at the gen_candidates() code @tacaswell shared the fallback chain ends with os.path.join(get_data_path(), 'matplotlibrc'), the packager-shipped default. Worth nailing down before a PR: should MATPLOTLIBNORC skip everything in that chain including the data-dir fallback, or only user/CWD-level candidates? That determines whether the env var means "no customization at all" vs "no user customization". @tacaswell raised this but it hasn't been settled yet.

    On the MATPLOTLIBRC=/dev/null point @timhoffm raised the code still yield 'matplotlibrc' first (CWD candidate) before hitting the env var branch, so that doesn't actually protect against the CWD lookup. Supporting MATPLOTLIBRC= or MATPLOTLIBRC=0 as a clean disable seems like a natural thing to bundle in.

    The PEP 687 analogy is a good framing btw worth keeping that in the migration guide/docs for security-conscious users.

  11. timhoffm commented on May 7, 2026

    @timhoffm
    Member

    @tacaswell We could make MATPLOTLIBRC=0 make also skipping the CWD check. In the end it comes down to the interpretation of MATPLOTLIBRC:

    • is ist just the one additional path in the list of possible locations? Then MATPLOTLIBRC=0 does not make sense.
    • is it a general configuration parameter controlling the loading of matplotlibrc files. Then we can support multiple values/types with different behaviors:
      • 0: Completely disable loading of an external matplotlibrc file.
      • explicit file: load this file
      • explcit folder: look for matplotlibrc in this folder
      • (optionally - if we feel the need to separately control is) HOME - only look in the XDG_CONFIG_HOME/HOME config folders

    I'm inclined to go with the second more general interpretation. To me this feels more natural than splitting into multiple parameters.

  12. kx7m2qd commented on May 7, 2026

    @kx7m2qd

    +1 to the second interpretation treating MATPLOTLIBRC as a general loading control is cleaner than adding a separate MATPLOTLIBNORC var.
    On the open question of scope: I'd suggest MATPLOTLIBRC=0 skips everything except the packager data-dir fallback (get_data_path()/matplotlibrc), since that's a system-level config rather than a user/project-level one. That way it means 'no user customization' rather than 'completely broken defaults'.
    Happy to open a PR once this is agreed the change in gen_candidates() would be straightforward.

  13. tacaswell commented on May 7, 2026

    @tacaswell
    Member

    My read is that this is trade off of "simpler" of only one key or "simpler" meaning of the values of multiple keys. I continue to think that two keys is simpler, but lets go with the consensus of one key with multiple meanings.


    The sentinel should be "set but empty" as '0' is a valid file name and directory name.

  14. rcomer commented on May 7, 2026

    @rcomer
    Member

    My reading of the OP is that it is intended to specifically prevent using a file in the current directory and that actively opting in via MATPLOTLIBRC should still be allowed.

    If a sysadmin sets MATPLOTLIBRC to the sentinel for everyone, is there anything to stop a user setting MATPLOTLIBRC to their own custom file? Then we still look in the current directory first, regardless of the sysadmin's intention.

  15. tacaswell commented on May 7, 2026

    @tacaswell
    Member

    We discussed this on the call:

    • probably no short-term path to changing default to "no matpltolibrc unless you opt in" (it is very long standing behavior and a very common pattern) so not worth bringing in a toggle ENV.
    • go with empty string as the sentinel on MATPLOTLIBRC= because it is not a valid filename and if it is empty opts out of all matplotlibrc (including the CWD version which technically has higher priority).
  16. kx7m2qd commented on May 8, 2026

    @kx7m2qd

    Makes sense. Empty string is clean since it's not a valid path. I'll take a look at the implementation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Security HardeningProactive security hardening. Existing vulnerabilities should be reported per our security policy

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions