Pillow, the de facto imaging library for Python and a transitive dependency of an enormous share of the PyPI ecosystem, shipped a fix in January 2022 for CVE-2022-22817, a vulnerability in its PIL.ImageMath.eval() function that allows an attacker who controls (or partially controls) the arguments passed to that function to achieve arbitrary code execution on the host running the affected application. Any codebase that calls ImageMath.eval() with an expression or environment influenced by untrusted input — think user-supplied image-processing pipelines, "compose your own filter" features, or math-expression driven image blending — is a candidate for exploitation. This post breaks down what the bug actually is, which Pillow releases are affected, what we know about severity and real-world exploitation, and how to remediate it, followed by how Safeguard helps teams catch dependencies like this before they ship.
What CVE-2022-22817 actually is
ImageMath is a long-standing Pillow module that lets developers evaluate simple mathematical expressions against image bands — for example, blending two images or computing a difference mask using a string like "a + b". Under the hood, ImageMath.eval() builds a Python eval() call, merging the caller-supplied environment dictionary (variable names mapped to image bands or values) with its own internal symbol table before executing the expression.
The problem is that eval() in Python is only as safe as the globals and builtins dictionary it is handed. ImageMath.eval() did not strip dunder (double-underscore) keys such as __builtins__ out of the caller-supplied environment argument before using it to construct the execution context. Python's eval() normally restricts access to builtins when it does not find a __builtins__ key in the globals dict it is given — but if an attacker can inject their own __builtins__ object into that environment (for example, a mapping like {"__builtins__": __builtins__}), the sandboxing effectively disappears. Once the full builtins namespace is reachable inside the evaluated expression, an attacker can call things like __import__("os").system(...), turning what looks like a constrained math-expression evaluator into a full Python code execution primitive.
In practice, exploitability depends on how an application uses ImageMath. A service is vulnerable if it exposes any path — directly or indirectly — where an attacker can influence the environment dictionary or the expression string passed into ImageMath.eval(). This is the kind of gap that is easy to introduce accidentally: a "custom formula" field in an image-editing API, a plugin system that lets users define blend modes, or even internal tooling that deserializes expressions from a config file an attacker can tamper with.
Affected versions and components
- Component:
PIL.ImageMathmodule within Pillow (the actively maintained fork of the original Python Imaging Library, distributed on PyPI as thePillowpackage and imported asPIL). - Affected versions: Pillow releases prior to 9.0.1. Because
ImageMath.eval()has existed in the codebase for a long time, the underlying design flaw predates any specific recent release — it was carried forward release after release until it was patched. - Fixed version: Pillow 9.0.1, which sanitizes the environment dictionary passed to
ImageMath.eval()so that dangerous dunder keys (including__builtins__) can no longer be smuggled in to restore full builtin access. - Ecosystem exposure: Pillow is one of the most widely depended-upon packages in the Python ecosystem — it backs image handling in web frameworks, data science pipelines, document generation tools, CMS platforms, and countless internal services. That breadth means CVE-2022-22817 is not confined to a niche corner of the ecosystem; it is the kind of transitive dependency that shows up several layers deep in SBOMs without engineering teams necessarily realizing
ImageMathis even in play.
Severity, exploit prediction, and known exploitation
CVE-2022-22817 is tracked by NVD with a CVSS v3.1 rating in the High range, reflecting that a successful exploit yields full compromise of confidentiality, integrity, and availability on the affected process, though it requires a code path that actually reaches ImageMath.eval() with attacker-influenced input — an access-complexity factor that keeps it from scoring as an unauthenticated remote-code-execution slam dunk in every deployment.
We are not aware of this CVE appearing in CISA's Known Exploited Vulnerabilities (KEV) catalog, and we have not seen credible public reporting of mass exploitation campaigns targeting it specifically. That said, absence from KEV is not a safety signal on its own — KEV tracks confirmed, observed exploitation, and a vulnerability like this one, buried in an imaging library, is exactly the kind of bug that can be exploited quietly in targeted intrusions against specific applications long before it shows up in aggregate threat intelligence. EPSS-style exploitation-probability scores for library-level bugs like this also tend to understate real-world risk, because the scoring models struggle to account for how deeply and unpredictably a component like Pillow is embedded across downstream applications.
The practical takeaway: do not calibrate urgency purely off KEV membership or a single probability score. If your application calls ImageMath.eval() anywhere near untrusted input, this bug deserves the same urgency as a directly reachable RCE, regardless of what the aggregate exploitation-prediction models say.
Timeline
- Design flaw introduced: The unsafe handling of the
environmentargument inImageMath.eval()was a long-standing characteristic of the module, present across many years of Pillow (and originally PIL) releases rather than something introduced by a single recent change. - Coordinated fix released: The Pillow maintainers shipped version 9.0.1 in January 2022, which addressed CVE-2022-22817 alongside other
ImageMath-related hardening fixes landed in the same release cycle. - CVE published: CVE-2022-22817 was assigned and published to describe the arbitrary-code-execution risk in
ImageMath.eval(), with the fix in 9.0.1 cited as the remediation. - Ongoing exposure window: Because Pillow is so frequently vendored, pinned, or bundled by downstream projects, organizations that do not actively track and upgrade transitive dependencies can remain exposed years after the official fix shipped — this is one of the more common patterns we see when auditing SBOMs today.
Remediation
- Upgrade Pillow to 9.0.1 or later. This is the definitive fix — it closes the specific injection path that let a crafted
environmentdictionary restore access to Python builtins insideImageMath.eval(). - Inventory every place
ImageMath.eval()is called, directly or through a dependency, and confirm none of them pass attacker-influenced strings or dictionaries as theexpressionorenvironmentarguments. Even after patching, treatingImageMath.eval()as safe-by-default for untrusted input is bad practice — prefer explicit, whitelisted image operations over dynamic expression evaluation wherever possible. - Audit transitive dependencies, not just direct requirements. Many services pull Pillow in indirectly through frameworks, PDF generators, thumbnailing libraries, or ML preprocessing stacks. A quick look at direct requirements will not surface this — you need full dependency-tree resolution or an SBOM to know whether a vulnerable Pillow version is actually present in the running artifact.
- Rebuild and redeploy affected container images and lockfiles. Pinned requirements files, lockfiles, or base container images built before January 2022 may still be shipping a pre-9.0.1 Pillow long after the source repository was updated — verify the resolved version in the artifact that actually runs in production, not just in the manifest.
- Add regression coverage if your application legitimately needs dynamic image-math expressions, ensuring any custom sandboxing you layer on top of Pillow is tested against builtin-injection techniques, not just the specific payload from the original advisory.
How Safeguard Helps
CVE-2022-22817 is a textbook example of the risk Safeguard is built to catch: a vulnerability in a widely vendored, transitively included library that quietly slips into production long after a patch exists, because no one owns the job of continuously reconciling what is declared against what is actually running. Safeguard's software supply chain security platform addresses exactly that gap.
- Continuous SBOM generation and dependency graph analysis surfaces Pillow — and every other transitive dependency — wherever it appears across your services, containers, and build artifacts, so a bug buried three layers deep in your dependency tree does not go unnoticed simply because it was not in your top-level manifest.
- Vulnerability correlation against your real, deployed artifacts means Safeguard flags CVE-2022-22817 specifically where an unpatched Pillow version is actually running, cutting through the noise of theoretical exposure to tell you which services need attention first.
- Reachability and usage context, where available, helps distinguish services that merely bundle Pillow from services that actually invoke risky code paths like
ImageMath.eval(), so remediation effort goes to the deployments with real exploitation potential first. - Build and registry provenance checks verify that once you bump Pillow to 9.0.1 or later, the fix actually lands in the container images and packages that reach production — closing the gap between patching the manifest and redeploying the fix.
- Policy-driven gating lets teams block builds or releases that reintroduce known-vulnerable Pillow versions, preventing regressions when a stale base image or cached layer creeps back into a pipeline.
Vulnerabilities like CVE-2022-22817 rarely announce themselves — they sit quietly in the dependency graph until someone finds the right code path to trigger them. Safeguard's job is to make sure your team finds that exposure first.