← Back to Feed CACHED · 2026-10-21 18:21:19 · CACHE_KEY CVE-2026-112403
CVE-2026-112403 · CWE-122 · Disclosed 2026-10-19

Pillow ImagingJpegDecode heap buffer overflow via SOF3 lossless JPEG draft scaling mismatch

ASSESSED — NOISGATE
Vendor
—
—
—
Reassessed
—
—
—
Verdict: —
Do you agree?
01 · The Real Story

Like ordering a small coffee and the barista force-pouring a venti into your espresso cup — Pillow's JPEG decoder never checks if the drink fits the glass

CVE-2026-112403 is a heap-based buffer overflow in Pillow's ImagingJpegDecode function, affecting all versions prior to 13.0.0. When an application calls Image.thumbnail() — one of Pillow's most commonly used APIs — it internally invokes JpegImageFile.draft() to request the JPEG decoder to produce a downscaled image (1/2, 1/4, or 1/8 size). Pillow allocates an output buffer sized for those *scaled* dimensions. However, when the input is a lossless SOF3 JPEG and the build is linked against libjpeg-turbo ≥ 3.0, the library silently ignores the scaling request and emits full-width scanlines. The result: full-resolution pixel rows are written into a buffer sized for a thumbnail. On images ≥ 1024×1024, this produces a controlled, repeated heap overflow that corrupts adjacent allocations and can potentially achieve code execution. The fix in Pillow 13.0.0 (commit 242a059a) adds post-decode dimension validation.

No official vendor CVSS was published by the Pillow project. Third-party aggregators scored this between 8.1 (HIGH, CVSS v3.1) and 9.2 (CRITICAL, CVSS v4.0). The CRITICAL label from CVSS v4.0 overstates the real-world risk. While the attack vector is network-reachable and requires no authentication, *reliable* code execution from a heap overflow in a CPython extension demands serious exploit-development effort — ASLR, heap randomization, and glibc hardening in modern Linux deployments are all working against the attacker. The bug is genuinely dangerous in server-side image-processing pipelines (Django, Flask, FastAPI apps accepting user-uploaded photos), but the path from "heap corruption" to "arbitrary code execution" is not free. A HIGH rating at 7.5 better reflects the current threat posture: massive attack surface, credible impact, but meaningful exploitation friction and zero weaponization evidence.

"Heap overflow via crafted JPEG in the most popular Python imaging library. Patch Pillow to 13.0.0."
02 · The Attack Path

5 steps from start to impact.

STEP 01

Craft a malicious SOF3 JPEG

The attacker constructs an 8-bit lossless JPEG (SOF3 marker) with dimensions ≥ 1024×1024. The SOF3 frame type is valid per the JPEG standard and will open normally in most viewers. The attacker controls pixel data content, dimensions, and component count — all of which influence the size and content of the overflow. Standard JPEG tools or a hex editor can produce SOF3 files; no specialized exploit framework is required at this stage.
Conditions required:
  • Attacker can craft arbitrary JPEG files
Where this breaks in practice:
  • SOF3 lossless JPEG is uncommon in the wild — WAF/upload validation rules that restrict to baseline JPEG (SOF0/SOF2) would block this input
Detection/coverage: Content-type and magic-byte validation at the upload endpoint can flag SOF3 JPEGs as anomalous; no mainstream scanner specifically signatures this yet.
STEP 02

Deliver JPEG to a Pillow-based processing endpoint

The attacker uploads the crafted JPEG to a web application that performs server-side image processing with Pillow. The target code path must invoke Image.thumbnail(), Image.draft(), or any wrapper that requests JPEG downscaling. This is extremely common: Django's ImageField with thumbnail generation, Flask-based APIs with image upload, content management systems, and ML data pipelines all routinely call thumbnail(). The attacker does not need authentication if the upload endpoint is public (e.g., profile photo upload, support ticket attachment).
Conditions required:
  • Target application accepts image uploads and processes them with Pillow's thumbnail() or draft() methods
  • Upload endpoint is reachable by the attacker (public or authenticated)
Where this breaks in practice:
  • Applications that only store images without resizing are not affected
  • Endpoints behind authentication narrow the attacker pool
Detection/coverage: Application-level logging of image processing operations; anomalous JPEG subtype (SOF3) in upload logs.
STEP 03

libjpeg-turbo 3.x ignores scaling, returns full-width rows

When Pillow's ImagingJpegDecode() calls jpeg_start_decompress() with a scale_denom > 1, libjpeg-turbo 3.x accepts the parameter but hardwires lossless streams to no scaling — the scale request is silently discarded. The decoder returns output_width equal to the original image width, but Pillow has already allocated the output Imaging buffer based on the *requested* scaled dimensions. Pillow does not verify output_width or output_components against the buffer it allocated.
Conditions required:
  • Pillow < 13.0.0 is installed
  • Pillow is linked against libjpeg-turbo ≥ 3.0 (bundled in standard PyPI wheels for Pillow 10.1+)
Where this breaks in practice:
  • Source-compiled Pillow on RHEL 8/9 or Ubuntu 22.04 LTS links against system libjpeg-turbo 2.x, which *rejects* SOF3 input entirely
  • Pillow installed in conda environments may use IJG libjpeg rather than libjpeg-turbo
STEP 04

Heap buffer overflow — full scanlines into undersized buffer

Each call to jpeg_read_scanlines() writes a full-width row (e.g., 4096 bytes for a 4096-wide RGB image) into a buffer allocated for the scaled width (e.g., 1024 bytes at scale_denom=4). This overflow is repeated for every scanline in the image, producing a large, controlled, sequential heap overwrite. Depending on heap layout, this corrupts adjacent allocations including Python object headers, function pointers, and other C-level structures. The process will almost certainly crash; achieving controlled code execution requires heap grooming.
Conditions required:
  • Steps 1-3 conditions met
Where this breaks in practice:
  • Modern glibc heap hardening (safe-unlinking, tcache key checks) makes reliable exploitation significantly harder than raw overflow suggests
  • ASLR and NX are default-on in all modern Linux distributions
  • CPython's object allocator (pymalloc) adds an additional layer of heap layout unpredictability
Detection/coverage: ASAN-instrumented Pillow builds detect the overflow immediately; crash logs with SIGSEGV in ImagingJpegDecode are a strong indicator.
STEP 05

Potential code execution in application context

If the attacker successfully grooms the heap to overwrite a function pointer or vtable in an adjacent allocation, they achieve code execution in the context of the Python application process. On typical web deployments this runs as www-data or a similarly unprivileged user, but with access to application secrets, database connection strings, environment variables, and internal network connectivity. On containerized deployments, the blast radius is contained to the container unless a container escape is chained.
Conditions required:
  • Attacker has developed a reliable heap-grooming primitive for the target environment
  • Target is not running under ASAN or other memory-safety instrumentation
Where this breaks in practice:
  • No public exploit or PoC exists — this step requires original exploit development
  • Heap layout varies across Python versions, OS versions, and libjpeg-turbo builds, making a universal exploit impractical
  • Container isolation, seccomp, and AppArmor further limit post-exploitation impact
Detection/coverage: EDR/HIDS detecting anomalous process behavior from the web server process (shell spawns, unexpected network connections); runtime application self-protection (RASP) tools.
03 · Compensating Control

1
HIGH 7.5→IGNORE 0.0
SEVERITY REDUCED
Upgrade Pillow to ≥ 13.0.0 across all environments — This is the definitive fix. pip install --upgrade Pillow>=13.0.0 resolves CVE-2026-112403 and 43 other security issues in the same release. Per the noisgate remediation SLA for HIGH, deploy within 180 days. Given the 44-CVE bundle, prioritize this upgrade — it is the single highest-ROI Python dependency update available right now. Pin the version in your requirements.txt or pyproject.toml and propagate through CI/CD.
2
HIGH 7.5→LOW 2.5
SEVERITY REDUCED
Validate uploaded image format before Pillow processing — reject SOF3 JPEG — Add pre-processing validation that reads the JPEG SOF marker byte and rejects SOF3 (lossless) frames. Lossless JPEG is extremely rare in user-uploaded content — blocking it has near-zero false-positive risk. This can be implemented as a ~10-line check before passing data to Pillow. Deploy within 30 days per the noisgate mitigation SLA for HIGH. This breaks the attack chain at Step 2 by preventing the trigger input from reaching the vulnerable code path.
3
HIGH 7.5→IGNORE 0.0
SEVERITY REDUCED
Pin Pillow builds to libjpeg-turbo 2.x where feasible — If upgrading to Pillow 13.0.0 is blocked by compatibility constraints, rebuilding Pillow from source against libjpeg-turbo 2.x eliminates the vulnerable code path entirely — libjpeg-turbo 2.x rejects SOF3 input. This is a viable interim control for environments that compile dependencies from source (e.g., Docker builds with apt or yum system libjpeg). Deploy within 30 days per the noisgate mitigation SLA for HIGH. Note: this sacrifices lossless JPEG support.
4
HIGH 7.5→MEDIUM 5.0
SEVERITY REDUCED
Run image-processing workloads in isolated containers with restricted syscalls — Containerize Pillow-based image processing with a minimal seccomp profile, read-only filesystem, no-new-privileges flag, and dropped capabilities. This does not prevent the heap overflow but limits the blast radius of successful exploitation to the container boundary. Pair with network policy restricting egress from image-processing pods. This reduces the Step 5 impact from 'application-tier RCE with network pivot potential' to 'contained crash or limited RCE with no lateral movement'.
What doesn't work
  • WAF JPEG content inspection — Standard WAFs inspect HTTP headers and request bodies but do not parse JPEG frame markers at the SOF level. A SOF3 JPEG has a valid image/jpeg MIME type and passes all standard content-type checks.
  • File extension filtering — The malicious file is a valid .jpg/.jpeg file. Extension-based filtering provides zero protection.
  • Image dimension limits alone — While images must be ≥ 1024×1024 to trigger the scaling code path, rejecting large images is impractical for most applications (1024×1024 is a normal photo size). A dimension cap low enough to prevent this bug would break legitimate functionality.
  • Python-level sandboxing (RestrictedPython, etc.) — The vulnerability is in Pillow's C extension (libImaging), not in Python code. Python-level execution restrictions do not apply to native code.
04 · Intelligence Metadata

The supporting signals.

In-the-Wild ExploitationNone observed. Not listed in CISA KEV. No GreyNoise, Shadowserver, or campaign reporting as of 2026-10-22. This is a library-level bug, not a network service, so mass scanning is not applicable — exploitation requires targeting specific applications.
Proof-of-ConceptNo public PoC found. No dedicated repos on GitHub matching CVE-2026-112403. Not indexed on pocindex.io. The SecureWithUmer/CVE-2026-PoCs collection does not include this CVE. Crafting a triggering SOF3 JPEG is straightforward; weaponizing the heap overflow into RCE is not.
EPSS0.00321 (0.321%) — 23.7th percentile. Low predicted exploitation probability over the next 30 days. Consistent with no known weaponization.
CVSS VectorsCVSS v3.1: 8.1 HIGH CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H (GitLab Advisory). CVSS v4.0: 9.2 CRITICAL CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N (GHSA). The AC:H in both vectors correctly reflects the heap exploitation complexity.
Affected VersionsAll Pillow versions < 13.0.0. The bug has existed since Pillow first added draft() JPEG scaling support, but only became *exploitable* when libjpeg-turbo 3.0 introduced SOF3 lossless decoding (August 2024). Pillow PyPI wheels bundle libjpeg-turbo ≥ 3.x starting from approximately Pillow 10.1+.
Fixed VersionPillow 13.0.0 (released 2026-10-16). Fix commit: 242a059a. Adds post-decode validation of output_width and output_components against the allocated buffer dimensions. Note: libjpeg-turbo also fixed the analogous TurboJPEG API issue in 3.1.4, but the Pillow-side fix is the authoritative remediation.
Exposure SurfacePillow has ~400M monthly PyPI downloads and is a transitive dependency of Django, Flask, Matplotlib, scikit-image, and thousands of other packages. However, only deployments that (a) process *untrusted* image input via thumbnail()/draft() AND (b) link against libjpeg-turbo 3.x are reachable. Standard PyPI wheels meet condition (b); source builds on enterprise LTS distros typically do not.
Disclosure Timeline2026-10-16: Pillow 13.0.0 released with fix. 2026-10-19: CVE-2026-112403 and GHSA-8g5g-8vmr-9782 published. 3-day window between patch availability and public disclosure.
ReporterNot publicly attributed in the GHSA advisory. The fix commit is authored by the Pillow core maintainers.
Related VulnsPart of a batch of 44 security fixes in Pillow 13.0.0, including CVE-2026-112400 (DoS), CVE-2026-112401 (GIF quadratic copy), CVE-2026-112404 (TIFF OOB read), CVE-2026-96639 (stale image storage). Organizations should treat the Pillow 13.0.0 upgrade as a security-critical bundle.

Sources.

  1. GHSA-8g5g-8vmr-9782 (GitHub Security Advisory)
  2. OSV — GHSA-8g5g-8vmr-9782
  3. GitLab Advisory — Pillow CVE-2026-112403
  4. Pillow 13.0.0 Release Notes
  5. Fix Commit 242a059a
  6. Strix.ai — CVE-2026-112403 Detail
  7. VulDB — CVE-2026-112403
  8. Pepy.tech — Pillow Download Statistics
05 · The Call

Final Verdict
= UNCHANGED to HIGH (7.5/10)

Why this verdict

  • Massive library footprint with common trigger path: Pillow averages ~400M monthly PyPI downloads and thumbnail() is among its most-called methods. Any Python web application accepting image uploads and generating thumbnails — Django, Flask, FastAPI, Celery workers — sits directly in the blast zone. The network-reachable, no-auth, no-interaction trigger path is legitimate.
  • PyPI wheels bundle libjpeg-turbo 3.x, widening the vulnerable population: The critical prerequisite (libjpeg-turbo ≥ 3.0) is *satisfied by default* for the majority of Pillow installations via pip install. This eliminates the most significant friction point for pip-based deployments. Source-built installs on RHEL 8/9 and Ubuntu 22.04/24.04 LTS linking against system libjpeg-turbo 2.x are *not* vulnerable, representing a meaningful but minority share of production deployments.
  • Heap exploitation complexity is real and unresolved: AC:H is correctly assigned. There is no public PoC, no weaponized exploit, and no evidence of in-the-wild exploitation. Transforming the heap overflow into reliable code execution against modern glibc (safe-unlinking, tcache hardening), ASLR, NX, and CPython's pymalloc allocator is a non-trivial research project. This is the primary reason the score sits at 7.5 rather than 8.1+.
  • Role multiplier — typical deployment role (application tier): Pillow is a library, not infrastructure. In its low-value role (developer workstations, Jupyter notebooks), exploitation impact is minimal. In its typical role (web application servers processing user-uploaded images), successful exploitation yields code execution as the web server user — access to app secrets, DB credentials, and internal network, but scoped to that application tier. In a plausible high-value role (CI/CD pipelines processing images from untrusted PRs, ML training pipelines ingesting external datasets), the blast radius extends to build secrets and artifact integrity — but this represents a small fraction (<5%) of Pillow's installed base. No canonical high-value infrastructure role (this is not a hypervisor, IdP, or network edge) → no floor override. The typical-role outcome (application-tier RCE) is serious but not fleet-scale.
  • No exploitation evidence, low EPSS: EPSS at 0.321% (23.7th percentile), not KEV-listed, no GreyNoise/Shadowserver signals. The 3-day head start between patch release and CVE publication further reduces the window for opportunistic exploitation.

Why not higher?

CRITICAL would require either active exploitation evidence or a reliable, low-complexity path to code execution. Neither exists. The heap overflow is real but heap grooming against modern mitigations (ASLR, glibc tcache hardening, pymalloc) has no published solution. Pillow is not canonically high-value infrastructure — it is a library running inside application processes, and even successful RCE is scoped to the application tier, not fleet-level or identity-level impact. The 9.2 CVSS v4.0 score from GHSA overweights the theoretical ceiling and underweights practical exploitation friction.

Why not lower?

MEDIUM would undervalue the combination of Pillow's enormous attack surface (~400M monthly downloads), the fact that PyPI wheels bundle the vulnerable libjpeg-turbo 3.x by default, and the realistic network-reachable trigger via commonplace thumbnail() calls on untrusted input. The vulnerability class (heap buffer overflow) has a credible path to code execution even if that path is complex. Downgrading below HIGH would be irresponsible for an image-processing library that is a transitive dependency of most Python web frameworks.

06 · Verification

Crowdsourced verification payload.

Run on any host where Pillow is installed. Execute with python check_cve_2026_112403.py — no special privileges needed. The script checks the installed Pillow version and the bundled libjpeg-turbo version to determine exploitability. Works in virtualenvs and system Python alike.

noisgate-verify.py
PYTHONREAD-ONLYSAFE
#!/usr/bin/env python3
"""check_cve_2026_112403.py — Detect CVE-2026-112403 (Pillow heap overflow in ImagingJpegDecode)
Outputs: VULNERABLE / PATCHED / UNKNOWN
Exit codes: 1 = VULNERABLE, 0 = PATCHED, 2 = UNKNOWN
"""
import sys

def main():
    try:
        from PIL import Image
        import PIL
    except ImportError:
        print("UNKNOWN — Pillow is not installed in this environment.")
        sys.exit(2)

    pillow_version = getattr(PIL, '__version__', '0.0.0')
    print(f"[*] Pillow version: {pillow_version}")

    # Parse version tuple
    try:
        parts = pillow_version.split('.')
        ver_tuple = tuple(int(x.split('dev')[0].split('a')[0].split('b')[0].split('rc')[0]) for x in parts[:3])
    except (ValueError, IndexError):
        print(f"UNKNOWN — Could not parse Pillow version '{pillow_version}'.")
        sys.exit(2)

    # Check if Pillow >= 13.0.0 (patched)
    if ver_tuple >= (13, 0, 0):
        print(f"PATCHED — Pillow {pillow_version} >= 13.0.0. CVE-2026-112403 is fixed.")
        sys.exit(0)

    # Pillow < 13.0.0 — check libjpeg-turbo version
    print(f"[*] Pillow {pillow_version} < 13.0.0 — checking libjpeg-turbo linkage...")

    ljt_version = None
    try:
        # Pillow exposes jpeg library info via features
        from PIL import features
        jpeg_info = features.version('libjpeg')
        if jpeg_info:
            print(f"[*] libjpeg version string: {jpeg_info}")
            # libjpeg-turbo reports versions like '3.0.0', IJG reports like '9e'
            try:
                ljt_parts = jpeg_info.split('.')
                if len(ljt_parts) >= 2:
                    ljt_major = int(ljt_parts[0])
                    ljt_version = ljt_major
            except ValueError:
                pass
    except Exception:
        pass

    # Also check via pillow_info for turbo indicator
    is_turbo = False
    try:
        from PIL import features
        is_turbo = bool(features.check('libjpeg_turbo'))
        if is_turbo:
            print("[*] libjpeg-turbo detected (turbo=True).")
        else:
            print("[*] libjpeg-turbo NOT detected (using IJG libjpeg or unknown).")
    except Exception:
        pass

    if ver_tuple < (13, 0, 0):
        if is_turbo and ljt_version is not None and ljt_version >= 3:
            print(f"VULNERABLE — Pillow {pillow_version} with libjpeg-turbo {ljt_version}.x.")
            print("  CVE-2026-112403: Heap buffer overflow in ImagingJpegDecode")
            print("  with SOF3 lossless JPEG when thumbnail()/draft() is used.")
            print("  Remediation: pip install --upgrade 'Pillow>=13.0.0'")
            sys.exit(1)
        elif is_turbo and (ljt_version is None or ljt_version < 3):
            print(f"PATCHED — Pillow {pillow_version} links libjpeg-turbo < 3.0.")
            print("  libjpeg-turbo 2.x rejects SOF3 input; not exploitable.")
            print("  Note: Upgrade Pillow to >= 13.0.0 anyway for 43 other fixes.")
            sys.exit(0)
        elif not is_turbo:
            print(f"PATCHED — Pillow {pillow_version} uses IJG libjpeg (not turbo).")
            print("  IJG libjpeg does not decode SOF3; not exploitable.")
            print("  Note: Upgrade Pillow to >= 13.0.0 anyway for 43 other fixes.")
            sys.exit(0)
        else:
            print(f"UNKNOWN — Pillow {pillow_version} < 13.0.0 but could not")
            print("  determine libjpeg-turbo version. Assume VULNERABLE and upgrade.")
            sys.exit(2)

if __name__ == '__main__':
    main()