GHSA-fw7q-ww8w-phx8
CVE Information
Summary
An attacker who controls, once, the
Get-Printer-Attributesresponse thatcupsdreceives while a driverless queue is created can causecupsdto write an attacker-chosen*cupsFilter2line 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-supportedcollection 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 bareelse):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 shippedlibcups2.4.18:
member value in memory output of ippAttributeString()x"x\\"x"+ LF +*cupsFilter2 ...+ LFidentical — LF passed through
ppd_read()incups/ppd.chas no backslash handling whatsoever (a backslash is an ordinary character), so the escaped"still closes the quote block. The attacker supplies no backslashes;libcupsadds them and the parser ignores them.3. Resulting PPD. Compiled from the real
v2.4.19tag 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: *EndLine 59 is a sibling of the legitimate seed
*cupsFilter2: "application/vnd.cups-pdf application/pdf 10 -".ppdOpenFile()accepts the file andcupstestppd -vreports no parse error, socupsdloads and uses it.4. From the PPD line to execution.
cups/ppd-cache.cappends everycupsFilter2value topc->filters;scheduler/printers.cpasses them toadd_printer_filter(), which accepts the 6-fieldsrc/type dst/type cost programform with an absolute path.scheduler/filter.c:91only replaces an existing entry when the newcostis strictly lower, and the always-present seed filter has cost0, so-1wins.5. Trigger and authorization (checked in v2.4.19).
_ppdCreateFromIPP()has exactly one call site —scheduler/ipp.c:5471insidecreate_local_bg_thread()— reached only from the new-queue branch ofCUPS-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 inSystemGroup(defaultroot lpadmin, requiring onlylpadmingroup).device-uriis restricted todnssd:// 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 bareelsewith:else if (ippValidateAttribute(attr) && (ippGetValueTag(attr) == IPP_TAG_BOOLEAN || ippGetValueTag(attr) == IPP_TAG_INTEGER || ippGetValueTag(attr) == IPP_TAG_KEYWORD))
attrhere is thejob-presets-supportedcollection attribute itself, not the loop'smember. Its value tag is always0x34(IPP_TAG_BEGIN_COLLECTION, measured with the shippedlibcups), 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 in5a2d2d4(2026-08-01). Both commits exist only onmaster—git tag --containsis empty andgit branch -r --containsreturns onlyorigin/master— so no release contains them. A value-type whitelist is also not a correct fix in principle, becauseippAttributeString()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 in8361420c(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:Abecause the default listener is loopback/domain-socket only, so adjacency or a locally placed queue is required.AC:Hbecause a driverless queue must be created against the attacker-influenced endpoint.PR:Lbecause queue creation needslpadmingroup, not root.C/I:Hfor arbitrary execution as the filter account;A:Lbecause that account is unprivileged. If you judge "someone must add this printer" to be user interaction, useUI:R(6.4). For a network-facing deployment the same code path reachesAV:N(7.1). Your assessment governs; I flag theAVambiguity 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.18andv2.4.19tags (cups/ppd-cache.c,cups/ppd.c,cups/ipp-support.c,scheduler/ipp.c,scheduler/filter.c,scheduler/printers.c,scheduler/job.c—git diff --numstat v2.4.18 v2.4.19reports no changes). The only file that differs anywhere inscheduler/isscheduler/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:sidwithapt-get install cups cups-client cups-daemon cups-filters(CUPS 2.4.18-1); the packagedcups/ppd-cache.cis byte-identical to thev2.4.18tag source (SHA-256dab16aad7dec092dd587f1dd697c2eecb745c6f4149e0c4aa269e06fcbbfbcd4).A. Malicious printer. Serve IPP Everywhere where the
job-presets-supportedcollection contains one preset with these members (values as they appear on the wire, value tagname):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/h3xC. Print and observe execution. With
/usr/local/bin/h3xa root-owned0755script that appendsidto/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_logand 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=/workD. Controls. The same preset with
cost 0still 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 victimcupsd, and a transparent proxy that rewrites only theGet-Printer-Attributesresponse (+93 bytes).
# Configuration cupsFilter2in the victim's PPDExecution A via intermediary, cost -1... -1 /usr/local/bin/h3xStarted filter ... (PID 5405), markeruid=7(lp)B via intermediary, cost 0... 0 /usr/local/bin/h3xnone 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 makesppd_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'sdocument-formatmust 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,
cupsddebug2logs, generated PPDs, control runs) is available on request, as is a patch if useful.Impact
An attacker who can influence, once, the
Get-Printer-Attributesresponse thatcupsdreceives while a driverless queue is created obtains persistent code execution as the CUPS filter account (lpon Linux; on the test platformuid=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
cupsdlistener is loopback/domain-socket only, so adjacency or a locally placed queue is required. I have not verified automatic queue creation viacups-browsedor 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/advisoriesentry. 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 aslpvia a modified queue PPD but through theippbackend's stderr status lines andfoomatic-rip; CVE-2026-34980 / CVE-2026-55467 inject via thecupsdCreateJoboption 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.