Safeguard
Vulnerability Analysis

CVE-2023-50447: Arbitrary code execution via Pillow Image...

A patch bypass in Pillow's ImageMath.eval() reopens arbitrary code execution first flagged in CVE-2022-22817. Here's what changed and how to remediate it.

Vikram Iyer
Security Researcher
8 min read

Pillow, the most widely used image-processing library in the Python ecosystem, shipped a fix in early 2024 for CVE-2023-50447, a vulnerability that allows arbitrary code execution through the ImageMath.eval() function. The flaw is a bypass of an older, previously-patched sandbox escape (CVE-2022-22817) in the same code path — meaning the original fix restricted one route to code execution while leaving another one open. Any application that evaluates image-math expressions built from user-supplied input, or that passes an attacker-influenced environment dictionary into ImageMath.eval(), is at risk of full code execution in the context of the Python process.

Because Pillow is a transitive dependency of an enormous share of the Python package ecosystem — pulled in by web frameworks, data science stacks, document generators, thumbnail services, and ML pipelines — this is exactly the kind of vulnerability that tends to sit quietly deep in a dependency tree, far from the application team that would otherwise be watching for it.

What ImageMath.eval() does, and how the bypass works

PIL.ImageMath.eval() is a utility that lets developers evaluate simple mathematical expressions against image data, for tasks like blending or combining bands from multiple images. Internally, it works the way many "safe eval" implementations do: it takes an expression string, builds a restricted namespace, and calls Python's built-in eval() against that namespace instead of the caller's full global environment.

Restricted-eval() designs are notoriously hard to get right, because Python's object model exposes many indirect paths back to dangerous built-ins (__builtins__, __import__, __subclasses__, and friends) unless every one of them is explicitly stripped out. Pillow had already been bitten by this once: CVE-2022-22817 showed that ImageMath.eval() could be coerced into calling __import__ and executing arbitrary code, and Pillow 9.0.0 shipped a fix that blocked that specific path.

CVE-2023-50447 is the sequel. The 9.0.0 fix constrained the expression namespace, but the function's environment parameter — the dictionary of additional variables a caller can supply for use inside the expression — was not sufficiently sanitized. An attacker who can influence the contents of that environment dictionary (directly, or through an application feature that forwards user-controlled values into it) can smuggle in objects or references that lead back to Python's built-ins, and from there to arbitrary code execution. In short: the sandbox around the expression string held, but the sandbox around the environment it evaluates against did not.

This distinction matters for triage. The vulnerability is not "any use of Pillow is exploitable" — it requires an application to pass attacker-influenced data into ImageMath.eval(), either as the expression itself or as part of the environment mapping. Applications that only use Pillow for standard operations like opening, resizing, or converting images and never touch ImageMath are not exposed to this specific issue. Applications that build dynamic image-processing pipelines, expression-driven filters, or "formula" fields exposed to end users are the ones that need to treat this as urgent.

Affected versions and components

  • Component: PIL.ImageMath.eval() in Pillow (the actively maintained fork of the Python Imaging Library, distributed as the Pillow package on PyPI, imported as PIL).
  • Affected versions: Pillow releases prior to 10.2.0, including versions that already contained the CVE-2022-22817 fix from 9.0.0 onward. In other words, upgrading to close the 2022 issue was not sufficient to close this one.
  • Fixed version: Pillow 10.2.0, which hardens ImageMath.eval() against the environment-based bypass.
  • Ecosystem exposure: Pillow is a foundational dependency across the Python packaging ecosystem — used by Django and Flask extensions, Jupyter/notebook tooling, document and PDF generation libraries, thumbnail and avatar services, and countless internal image-handling scripts. Because it's so often a transitive dependency, teams frequently don't realize how many services in their environment ultimately import it.

CVSS, EPSS, and KEV context

NVD's published record for CVE-2023-50447 rates this a High-severity issue, reflecting the fact that successful exploitation can lead to full arbitrary code execution — the worst-case outcome for a confidentiality/integrity/availability standpoint — while the requirement that an application actually funnel attacker-controlled data into ImageMath.eval() keeps it from reaching a Critical rating in isolation.

We'd rather point you to the authoritative source than repeat a number that may drift out of date: check the current CVSS vector and score on NVD's CVE-2023-50447 entry, and pull the live exploitation-probability estimate from FIRST.org's EPSS API, since EPSS scores are recalculated daily as real-world exploitation signals change. As of this writing, we are not aware of CVE-2023-50447 appearing in CISA's Known Exploited Vulnerabilities (KEV) catalog, but KEV status can change, and it's worth checking directly if this component is in your environment.

Regardless of where the score lands on a given day, the practical risk driver here is architectural: does anything in your stack pass untrusted strings or dictionaries into ImageMath.eval()? If yes, treat this as high urgency independent of the numeric score.

Timeline

  • January 2022 — Pillow 9.0.0 ships a fix for CVE-2022-22817, closing an earlier ImageMath.eval() code-execution path involving __import__.
  • Late 2023 — Security researchers identify that the environment-parameter path around ImageMath.eval() was not covered by the original fix, allowing the same class of arbitrary code execution to be reached through a different route. CVE-2023-50447 is assigned to track the bypass.
  • January 2024 — Pillow 10.2.0 is released, incorporating a fix that further restricts what the environment argument can expose to the evaluated expression.
  • Ongoing — Because Pillow is such a heavily-depended-upon package, downstream projects and Linux distributions have progressively picked up 10.2.0+ in their own release cycles; environments still pinned to older Pillow releases remain exposed.

Remediation steps

  1. Upgrade Pillow to 10.2.0 or later across every service, container image, virtual environment, and lambda/function bundle that includes it — not just your primary application repos. Because Pillow is so often pulled in transitively, run a full dependency inventory rather than trusting requirements.txt alone.
  2. Regenerate and rebuild container images and deployment artifacts after bumping the version. A patched requirements.txt that hasn't been rebuilt into the running image provides no protection.
  3. Audit any code that calls ImageMath.eval() directly, and specifically look for cases where the expression string or the environment dictionary is built from request parameters, form fields, uploaded metadata, or any other user-influenced input. Even on a patched version, treat user-controlled input to ImageMath.eval() as a code-injection surface and validate/allowlist it defensively.
  4. Check for pinned or vendored copies of Pillow, including in Docker base images, CI runner images, and internal artifact mirrors, which can silently reintroduce a vulnerable version even after your application manifest is updated.
  5. Verify the fix landed by confirming the installed version (python -c "import PIL; print(PIL.__version__)") in the actual deployed runtime, not just in source control — configuration drift between what's committed and what's running is one of the most common reasons "already patched" vulnerabilities keep showing up in scans.

How Safeguard Helps

CVE-2023-50447 is a textbook example of why point-in-time dependency audits aren't enough: it's a bypass of a fix for a different CVE in the same function, in a package that's usually several layers deep in a dependency tree rather than sitting in a top-level manifest. Safeguard is built to catch exactly this pattern.

  • Transitive dependency visibility: Safeguard maps your full software bill of materials, including indirect dependencies like Pillow pulled in through frameworks, plugins, or internal libraries, so a vulnerability like this surfaces even when no team explicitly declared Pillow as a direct requirement.
  • Continuous vulnerability monitoring: as new CVEs and advisories are published against packages already running in your environment, Safeguard flags the affected components automatically, so a bypass disclosed against code you thought was already remediated doesn't sit undetected.
  • Runtime and build-artifact verification: Safeguard checks what's actually installed in built images and deployed workloads, not just what's declared in source, closing the gap between "we upgraded the manifest" and "the running service still has the vulnerable version."
  • Risk-based prioritization: rather than treating every CVE as equally urgent, Safeguard correlates severity signals with actual reachability — for example, flagging with higher priority the services where ImageMath is actually invoked with dynamic input — so security and engineering teams can focus remediation effort where exploitation is realistic.
  • SOC 2 and audit-ready evidence: for teams that need to demonstrate vulnerability management maturity to auditors or customers, Safeguard maintains a continuous record of when a CVE was detected, when it was remediated, and what evidence supports that remediation, turning incidents like this one into documented, closed-loop processes rather than one-off fire drills.

Supply chain vulnerabilities like CVE-2023-50447 don't announce themselves — they wait in a dependency graph until someone builds a feature that happens to touch the vulnerable code path. Continuous, dependency-aware monitoring is what turns that into a routine patch cycle instead of an incident.

Never miss an update

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