| Package | Affected versions | Patched versions |
|---|---|---|
| pillow | < 13.0.0 | 13.0.0 |
============================================================================
Memory disclosure and segfault in libtiff encoder via crafted TIFF tags
Location: src/encode.c:1014 in PyImaging_LibTiffEncoderNew()
Commit: a707c6b21c313880d0771e54965d142dd42358b6
============================================================================
A crafted TIFF file of a few hundred bytes makes Pillow copy an attacker-chosen amount of its own process memory into the TIFF it writes back out, or crash the process outright. The trigger is an ordinary Image.open() followed by a bare im.save(fp, "TIFF"). The only precondition is that the application re-saves images as TIFF. When the uploaded file is compressed, Pillow routes that save through libtiff automatically, with no options from the application. The attached reproducer does this with a 356-byte LZW TIFF: one crafted tag comes back holding 65536 bytes taken from _imaging.so's read-only data, and a second crafted tag makes the worker die with SIGSEGV on every run.
The bug is in PyImaging_LibTiffEncoderNew (src/encode.c). It decides what arguments to pass to the variadic TIFFSetField() from the shape of the Python value (scalar, tuple or bytes). It assumes its ImagingLibTiffMergeFieldInfo() call defined how libtiff will read them. But TIFFMergeFieldInfo() never overrides libtiff's built-in field table, so libtiff reads the arguments according to its own definition of the tag:
Where that unwritten slot points depends on the compiler. In the tested clang build it points into _imaging.so, and a larger count returned thousands of pointers into libpython, libc and libtiff, which defeats address-space randomisation for anyone chaining another bug. When the application preserves metadata with tiffinfo=im.tag_v2, a 64-bit entry turns ActiveArea into a 16-byte read from an address the attacker chooses. All observed memory errors are reads; no out-of-bounds write was found.
Other branches of the same loop share the defect and are reachable from crafted TIFFs or from EXIF in JPEG/PNG/WebP re-saved as compressed TIFF. These are the bytes, RATIONAL-tuple and typed-tuple branches, and a BYTE-typed ColorMap. The file-reachable forwarding was introduced in a3d45e9cefd0 ("Added custom int and float TIFF tags") and first released in 5.4.0.
PyImaging_LibTiffEncoderNew (src/encode.c:777-1060) chooses the argument
list it hands to the variadic TIFFSetField() from the Python value of each
tag — scalar vs. tuple vs. bytes, and a type that is either autodetected or
copied from the source file's IFD — and assumes that its own
ImagingLibTiffMergeFieldInfo() registration defines how libtiff will read the
va_list. It does not: libtiff keeps its built-in definition for every tag it
already knows, and core tags are never registered at all. libtiff therefore
consumes the varargs as (count, pointer), (pointer) or three pointers
according to its field table, whatever Pillow actually pushed. The encoder
never consults that table (TIFFFindField / TIFFFieldPassCount /
TIFFFieldWriteCount / TIFFFieldDataType) before the call.
The most severe instance, and the one the attached reproducer exercises: when a
TIFF is re-saved through the libtiff writer, every tag of the source IFD that
Pillow does not recognise is forwarded to libtiff with TIFFSetField(). A
count-1 integer entry is passed as a single scalar vararg. If libtiff's own
field table defines that tag number as an array field, libtiff consumes the
varargs as (count, pointer) or (pointer) instead, so the attacker's integer
becomes either the number of bytes to copy from a never-written vararg slot, or
the source address itself. The same mismatch is reachable through the typed
tuple branches (tuple length consumed as a pointer for fixed-count built-in
tags such as CFARepeatPatternDim, even when the source file is well-formed),
the bytes, TIFF_RATIONAL-tuple and core-tag branches of the same loop;
those are listed
under Other branches with the same defect below and need to be covered by the
same fix.
A 181-byte TIFF (16×16 RGB, one LZW strip, plus one extra IFD entry
50740 / LONG / count 1 / value 4096 — DNGPrivateData) opened with
Image.open() and saved with a bare im.save(buf, "TIFF") produces a 4296-byte
output whose tag 50740 holds 4096 bytes that occur nowhere in the input; scanning
/proc/self/mem finds them in the read-only data segment of _imaging.so.
Changing the extra entry to 50829 / LONG / 1 / 64 (ActiveArea) makes the same
two calls segfault in memcpy(dst, src=0x40, n=16) under
ImagingLibTiffSetField ← PyImaging_LibTiffEncoderNew. An ASan build of the
extensions reports READ of size 4096 in memcpy called from
src/encode.c:1014 for the first input.
_save copies all tags of the opened image into the libtiff attribute list.
For a tag that is neither in LIBTIFF_CORE nor in TiffTags.TAGS_V2, no type
is recorded, so the value goes through untyped:
# src/PIL/TiffImagePlugin.py:1782
supplied_tags = {**legacy_ifd, **getattr(im, "tag_v2", {})}
…
# :1959-1982
for tag, value in itertools.chain(ifd.items(), supplied_tags.items()):
if tag not in TiffTags.LIBTIFF_CORE:
…
elif isinstance(value, (int, float, str, bytes)) or …:
type = TiffTags.lookup(tag).type # None for 50740, 50829, …
if type:
types[tag] = type
if tag not in atts and tag not in blocklist:
…
atts[tag] = value # plain int 4096
compression defaults to the source image's im.info["compression"]
(TiffImagePlugin.py:1731-1746), so a compressed source selects the libtiff
writer without the application asking for it.
In the encoder the untyped int is autodetected as TIFF_LONG
(src/encode.c:805-815), the tag is "registered" as a scalar custom field, and
then set with one 32-bit vararg:
// src/encode.c:844 (is_var_length = 0 → writecount 1, passcount 0)
if (ImagingLibTiffMergeFieldInfo(&encoder->state, type, key_int, is_var_length)) {
… continue;
}
…
// src/encode.c:1013-1016
} else if (type == TIFF_LONG || type == TIFF_IFD) {
status = ImagingLibTiffSetField(
&encoder->state, (ttag_t)key_int, (UINT32)PyLong_AsLong(value)
);
The registration does not do what the caller assumes. TIFFMergeFieldInfo()
only adds definitions for tags libtiff does not already know and returns 0
(success) either way, so for tags in libtiff's built-in table the built-in
definition stays in force. Queried against the linked libtiff 4.5.0 after
exactly Pillow's merge call:
50740 DNGPrivateData type=BYTE passcount=1 writecount=-1 (unchanged)
50829 ActiveArea type=LONG passcount=0 writecount=4 (unchanged)
65000 CustomField type=LONG passcount=0 writecount=1 (Pillow's definition, tag was unknown)
libtiff's setter for FIELD_CUSTOM tags then interprets the va_list
according to its definition: for passcount=1 it reads va_arg(ap, int) as
the element count — the attacker's 4096 — allocates count * size bytes and
memcpys that many bytes from va_arg(ap, void *), a slot
PyImaging_LibTiffEncoderNew never wrote. On x86-64 that slot is register
rcx; in this build it holds a pointer into _imaging.so's .rodata, so the
copy succeeds deterministically (a value of 2 000 000 also succeeds and yields a
2 000 200-byte output). TIFFWriteDirectory writes the copied bytes into the
output file as the tag's value. For fixed-count array fields (passcount=0,
writecount=4 for ActiveArea) there is no count argument, so the attacker's
scalar is consumed as the void * source and 16 bytes are read from address
64 → SIGSEGV.
Tags in libtiff 4.5's built-in table that Pillow forwards untyped and that take
the (count, pointer) form include DNGPrivateData 50740, CFAPattern 33422,
CFAPlaneColor 50710, LinearizationTable 50712, BlackLevel 50714, WhiteLevel
50717, ColorMatrix1/2 50721/50722, AsShotNeutral 50728, OriginalRawFileName/Data
50827/50828, AsShotICCProfile 50831, CurrentICCProfile 50833. Fixed-count ones
(pointer only → crash) include ActiveArea 50829, DNGVersion/DNGBackwardVersion
50706/50707, DefaultScale/DefaultCropOrigin/DefaultCropSize 50718–50720,
AsShotWhiteXY 50729, CFARepeatPatternDim 33421, BlackLevelRepeatDim 50713,
ImageLayer 34732, RawDataUniqueID 50781.
When the application saves with tiffinfo=im.tag_v2 (the documented way to
preserve metadata), the file-declared type is honoured, and a LONG8 entry
reaches the (uint64_t)PyLong_AsLongLong(value) branch at
src/encode.c:1049-1052. For ActiveArea that is a 16-byte read from a full
64-bit attacker-chosen address into the output: a source entry
50829 / LONG8 / 1 / <address of a Python bytes object> produced an output
ActiveArea of (b'SECR', b'ET-T', b'OKEN', b'-012') in testing.
The comment above blocklist += at TiffImagePlugin.py:1938-1944 already notes
that array tags with the wrong length "will result in segfaults"; the blocklist
covers only a handful of tags Pillow itself knows, not the ones libtiff knows and
Pillow does not. Forwarding of these tags was introduced in a3d45e9cefd0
("Added custom int and float TIFF tags", 2018-10-25), first released in 5.4.0;
releases 2.0.0–3.0.0 forwarded them as scalars too, until 9fa6634f4
("Partial fix for #1597") limited _save to LIBTIFF_CORE.
The scalar-integer branch above is one of several places in the same loop
where the call shape is derived from the Python value rather than from
libtiff's definition. All of the following were confirmed on this commit (stock
build crashes and/or ASan reports, stack always memcpy ← libtiff ← ImagingLibTiffSetField src/libImaging/TiffDecode.c:934 ← PyImaging_LibTiffEncoderNew), and all are reachable from a sub-1 KB crafted
file followed by an ordinary save():
(len, av) for a fixed-count built-in field
(src/encode.c:904-994, the SHORT/LONG/SBYTE/SSHORT/SLONG/FLOAT/DOUBLE
tuple branches all call ImagingLibTiffSetField(..., len, av)). For libtiff
built-in fields with passcount=0 and a fixed writecount —
CFARepeatPatternDim 33421 (SHORT×2), BlackLevelRepeatDim 50713
(SHORT×2), ImageLayer 34732 (LONG×2), ActiveArea 50829 (LONG×4),
MatrixWorldToScreen/Camera 33305/33306 (FLOAT×16) — libtiff takes the
first vararg as the source pointer, so the tuple length becomes the
memcpy source address. A perfectly well-formed 206-byte 8×8 PackBits TIFF
carrying 33421 / SHORT / 2 / (2, 2) re-saved with a bare
im.save(out, "TIFF") dies with SIGSEGV in memcpy (gdb: rsi = 0x2, the
tuple length) on every run; so does
Image.new("L", (8, 8)).save(buf, "TIFF", compression="packbits", tiffinfo={33421: (2, 2)}), and a JPEG whose EXIF carries the tag saved with
exif=im.getexif() into a compressed TIFF. bytes values for built-in
fixed-count BYTE tags (e.g. DNGVersion 50706, BYTE×4) crash the same way
through the (PyBytes_Size, PyBytes_AsString) call at src/encode.c:860-865.
The mirror image for passcount=1 fields: Photoshop 34377 declared
SHORT, count 1, value 60000 takes the scalar branch at :1009-1012, libtiff
reads 60000 as the count and an unpassed vararg as the pointer, and the
re-save succeeds with a 60162-byte output whose tag 34377 holds 60000 bytes
of process memory (here C prototype strings from _imaging.so's .rodata).bytes value → single char * (src/encode.c:805-815 autodetects
bytes as TIFF_ASCII; :1033-1042 passes PyBytes_AsString(value) alone).
ImageFileDirectory_v2._setitem stores a BYTE-typed entry as bare bytes
and an ASCII-typed one as str (encoded to bytes by _save), so the
file's declared type selects this branch.
ColorMap (320) declared BYTE with 4 bytes of data in a non-palette
PackBits TIFF: 320 is in core_tags[], no type is consulted, the value
skips the 768-tuple TIFFTAG_COLORMAP branch (:871-895) and libtiff reads
three uint16_t * and copies 1 << BitsPerSample shorts from each — a
512-byte over-read of the 5-byte Python bytes buffer, then dereference of
two stale argument registers → SIGSEGV on every run of
Image.open(f); im.save(out, "TIFF"). COLORMAP is not in _save's
blocklist.Photoshop (34377) declared ASCII: _save copies value and type
(TiffImagePlugin.py:1802-1818; only IPTC_NAA_CHUNK has a BYTE/UNDEFINED
type guard there, from commit 2245df0a — PHOTOSHOP_CHUNK and XMP do
not). libtiff's definition is BYTE, passcount=1, so the low 32 bits of
the char * become the byte count for a memcpy from a stale register
(ASan: READ of size 3518384; stock: SIGSEGV in about half of runs,
otherwise RuntimeError). ICCProfile (34675) declared ASCII behaves the
same way via im.info["icc_profile"] → ifd[ICCPROFILE]
(TiffImagePlugin.py:1822-1824).TIFF_RATIONAL tuple → array with no count (src/encode.c:995-1006;
compare the TIFF_FLOAT branch at :969-981, which passes len, av).
passcount=1 (TiffDecode.c:904-908), so libtiff takes the low 32 bits of
av as the element count and the next stale vararg as the source:
heap-buffer-overflow READ of size 119872 … right of 8-byte region under
ASan (119872 == 4 * low32(av)), SIGSEGV in ~4 of 5 stock runs. Reached by
a JPEG whose EXIF carries one private RATIONAL×2 tag saved with
im.save(out, "TIFF", compression="tiff_lzw", exif=im.info["exif"]), or a
TIFF re-saved with tiffinfo=im.tag_v2 (_setitem auto-types a tuple of
IFDRational as RATIONAL, TiffImagePlugin.py:689-701; types[tag] = ifd.tagtype[tag] at :1968).WhitePoint (318, 2),
PrimaryChromaticities (319, 6) and YCbCrCoefficients (529, 3), which
_save forwards from any source TIFF and types RATIONAL via
TiffTags.lookup, the pointer-only convention happens to match, but
libtiff copies its fixed count from an array that is only as long as the
file declared: a PackBits TIFF with PrimaryChromaticities count 1 and a
bare im.save(out, "TIFF") gives READ of size 24 … right of 4-byte region, and the over-read floats are written to tag 319 of the output.
REFERENCEBLACKWHITE/TRANSFERFUNCTION are blocklisted at
TiffImagePlugin.py:1941-1952 for exactly this reason; 318/319/529 are not.im.save(out, "TIFF", tiffinfo=im.tag_v2), _save copies
ifd.tagtype[key] = info.tagtype[key] for every key
(TiffImagePlugin.py:1768-1776) and later assigns its own values to some of
the same tags without resetting the type (ifd[COLORMAP] = colormap at
:1870-1877; likewise BitsPerSample, SamplesPerPixel, ExtraSamples,
ICCProfile, …). For a 418-byte PackBits palette TIFF whose ColorMap entry
is declared BYTE, _setitem (:751-770) truncates _save's 768-entry
palette to a single int (with only a "too many entries" warning), the
encoder receives (320, 0), autodetects TIFF_LONG, and
src/encode.c:1013-1016 calls TIFFSetField(TIFFTAG_COLORMAP, (UINT32)0) —
libtiff reads three uint16_t * and faults at an address ≤ 65280 on every
run. The stale-tagtype copy is a Python-side hygiene bug in its own right,
but its memory-safety consequence exists only because the encoder lets a
non-tuple value reach TIFFSetField(COLORMAP).Any service that accepts a TIFF and writes it back out as TIFF through libtiff —
which happens with a plain im.save(fp, "TIFF") whenever the uploaded file is
compressed, or whenever a compression= other than raw is requested — returns
to the uploader, inside one metadata tag of the produced file, an
attacker-chosen number of bytes read from a stale pointer in the worker process,
deterministically and without retries. Which memory that is depends on the
compiler's register allocation for PyImaging_LibTiffEncoderNew: in this clang
build the pointer is _imaging.so + 0x1588, so a request for 262 144 bytes
(which succeeded every time in testing, as did 2 000 000) covers the
extension's .rodata, .data.rel.ro/GOT and .data and returned 2 800+
resolved pointers into libpython, libc, libtiff, libjpeg and others — a complete
ASLR bypass for chaining with any other memory bug; a gcc build of the same
source left a different, unrelated address in that slot. The fixed-count
variant kills the worker process with SIGSEGV from a 181-byte upload on every
request. With tiffinfo=im.tag_v2 the source
address is fully attacker-chosen (16-byte arbitrary read into the output for
ActiveArea, or a crash). No non-default Pillow setting is involved; the only
precondition is TIFF-to-TIFF re-encoding.
The other branches add further deterministic process-kill inputs (BYTE-typed
ColorMap in any RGB or palette TIFF; thumbnail() keeps tag_v2, so
open→thumbnail→save-as-TIFF is affected too) and extend the reach to
JPEG/PNG/WebP uploads whose EXIF is preserved with exif= when converting to
libtiff-compressed TIFF. All the memory errors are reads; libtiff sizes its
destination buffers with the same count it copies, so no out-of-bounds write
was found.
repro is one Python script, run against the stock build with the installed PIL. It needs no compiler and no extra files.
upload(tag, value) has Pillow write a 16×16 red RGB image as an LZW-compressed TIFF, then appends a new IFD holding the same entries plus one extra <tag> / LONG / count 1 / value (kept in tag order) and points the header at it — a 356-byte input on this tree. The source is LZW-compressed on purpose. A bare im.save(out, "TIFF") keeps the source's compression, so the save goes through the libtiff writer even though the application passes no arguments. resave() is the whole "application": Image.open(BytesIO(src)); im.save(out, "TIFF").
The script runs three cases:
4096. This is what should happen to every tag.bytes of length 65536 that do not occur in the input. The script prints the first 32 of them and the file offset at which they occur in _imaging.so on disk. On this build they always do (offset 0x57588): the stale pointer aims into the extension's read-only data.repro child, core dumps disabled) to do this re-save, and the child exits with -11, meaning SIGSEGV.Expected output on the affected tree:
control tag 65000=4096 -> 4096
leak tag 50740=65536 from a 356-byte upload -> 65536 bytes not present in input, head a661fcffc661fcff3360fcff7b61fcffd361fcff085ffcffe061fcff3360fcff
leaked bytes found in _imaging.so at file offset 0x57588
crash tag 50829=64 -> child exit -11 (SIGS
Running one of these products? Scan your site to see whether it exposes a vulnerable version.
Scan my site free