Skip to content

GHSA-7hqf-mfhx-7r3v

CVE Information

Summary

A malicious or attacker-controlled IPP printer can return a crafted text printer-status attribute, such as marker-message, containing embedded newline characters. The CUPS ipp backend reports this remote printer attribute to the scheduler through the backend stderr/status stream. The current quoting helper escapes quotes and backslashes, but it does not reject or encode LF/CR. As a result, attacker-controlled printer status text can create a new backend status line beginning with PPD:.

The scheduler treats backend status lines beginning with PPD: as requests to update the local queue PPD. In my lab validation, a remote marker-message injected:

PPD: FoomaticRIPCommandLine=...

This was written into:

/etc/cups/ppd/<queue>.ppd

When the queue used a Foomatic-backed PPD/filter profile, a later print job caused foomatic-rip to read the modified PPD and execute the injected harmless marker command as the CUPS filter user lp.

This report does not claim default unauthenticated root RCE. The confirmed impact is conditional code execution as user lp.

Affected Code Locations

The issue appears to involve the CUPS ipp backend status reporting path, the scheduler backend-status parser, and the PPD update path.

In the reviewed source snapshot, the relevant locations are:

  1. backend/ipp.c

The ipp backend requests marker-message from the remote IPP printer:

  • backend/ipp.c:89-105
  • backend/ipp.c:100: marker-message

The backend later reads and reports marker-message:

  • backend/ipp.c:3298-3318
  • backend/ipp.c:3310-3312:

ippFindAttribute(ipp, "marker-message", IPP_TAG_TEXT)

report_attr(marker)

The vulnerable quoting/reporting path is:

  • backend/ipp.c:3068-3114: quote_string()
  • backend/ipp.c:3094-3106: only backslash, double quote, and single quote are specially escaped before copying bytes into the output buffer.
  • backend/ipp.c:3106: *qptr++ = *s++;

There is no rejection or encoding of LF/CR before the string is returned.

The attribute reporting path is:

  • backend/ipp.c:3121-3184: report_attr()
  • backend/ipp.c:3149-3155: IPP_TAG_TEXT, IPP_TAG_TEXTLANG, IPP_TAG_NAME, IPP_TAG_NAMELANG, and IPP_TAG_KEYWORD values are passed through quote_string(...).
  • backend/ipp.c:3178-3180: the final value is emitted to backend stderr/status as:

fprintf(stderr, "ATTR: %s=%s\n", attr->name, value);

Because LF/CR inside the quoted value are preserved, a remote text attribute can split the backend status stream into multiple logical scheduler lines.

  1. scheduler/statbuf.c

The scheduler status buffer parses backend stderr/status output line by line:

  • scheduler/statbuf.c:110-145: cupsdStatBufUpdate(...) reads from the backend status pipe and searches for newline using strchr(sb->buffer, '\n').
  • scheduler/statbuf.c:262-266: lines beginning with PPD: are classified as CUPSD_LOG_PPD.

Relevant code:

else if (!strncmp(sb->buffer, "PPD:", 4))

*loglevel = CUPSD_LOG_PPD;

message = sb->buffer + 4;

This means a newline injected into an ATTR: value can create a later independent PPD: line.

  1. scheduler/job.c

The scheduler handles CUPSD_LOG_PPD messages as PPD keyword updates:

  • scheduler/job.c:5436-5461
  • scheduler/job.c:5445: logs PPD: %s
  • scheduler/job.c:5448: parses the message using cupsParseOptions(...)
  • scheduler/job.c:5456-5457: filters some CUPS-specific special PPD keywords, then adds all other keys to job->keywords.

The current blacklist includes keys such as:

  • cupsFilter
  • cupsFilter2
  • cupsPortMonitor
  • cupsPreFilter
  • APPrinterPreset

However, the reproduced chain uses the third-party Foomatic key:

FoomaticRIPCommandLine

This key is not filtered by this CUPS-specific blacklist and is therefore preserved in job->keywords.

  1. scheduler/job.c job completion path

The collected PPD updates are applied later:

  • scheduler/job.c:3624-3636
  • scheduler/job.c:3628-3631:

if (job->num_keywords)

cupsdUpdatePrinterPPD(job->printer, job->num_keywords, job->keywords)

  1. scheduler/printers.c

The PPD update function writes accepted keyword/value pairs into the local queue PPD:

  • scheduler/printers.c:2838-2862: cupsdUpdatePrinterPPD(...)
  • scheduler/printers.c:2862: builds /etc/cups/ppd/<queue>.ppd
  • scheduler/printers.c:2891-2895: writes each accepted keyword into the PPD:

cupsFilePrintf(dst, "*%s: %s\n", keyword->name, keyword->value);

This is where the attacker-controlled FoomaticRIPCommandLine value is persisted into the local queue PPD.

  1. Downstream Foomatic execution path

The modified PPD is later consumed by foomatic-rip.

Relevant downstream locations:

  • cups-filters/filter/foomatic-rip/options.c
  • FoomaticRIPCommandLine is read as a renderer command line.
  • cups-filters/filter/foomatic-rip/postscript.c
  • the renderer command line is built and started during the filter path.

In my previous source review, the downstream execution evidence maps to the foomatic-rip renderer command path where the command line is logged as:

Starting renderer with command: "..."

Root Cause

The root cause is a status-stream injection across trust boundaries.

The remote IPP printer controls a text attribute value. That value is written by the CUPS ipp backend into a scheduler-consumed backend status stream. The backend quoting helper escapes quotes and backslashes, but it preserves LF/CR. The scheduler then interprets the injected newline as a new backend status message.

The attacker-controlled value has this logical shape:

marker-message = "status-line-before\nPPD: FoomaticRIPCommandLine=cat;/usr/bin/id>/tmp/<marker>;/bin/chmod${IFS}0644${IFS}/tmp/<marker>;#\nDEBUG: status-line-after"

The backend status stream is then interpreted as separate lines:

ATTR: marker-message='"status-line-before

PPD: FoomaticRIPCommandLine=cat;/usr/bin/id>/tmp/<marker>;...

DEBUG: status-line-after"'

The injected PPD: line is treated as a PPD update instruction. The scheduler filters several CUPS-specific dangerous PPD keys, but it does not block FoomaticRIPCommandLine. The accepted key/value is stored in job->keywords and later written into the local queue PPD by cupsdUpdatePrinterPPD().

When a later print job uses the modified PPD, foomatic-rip reads FoomaticRIPCommandLine and executes the renderer command line. In my lab validation, the command only wrote a harmless /tmp marker containing the execution UID.

Reproduced Call Chain

  1. Target CUPS host has an IPP queue pointing to an attacker-controlled or compromised IPP printer.
  2. A print job causes the CUPS ipp backend to query the remote printer.
  3. The malicious printer returns a marker-message text value containing LF-separated status lines.
  4. backend/ipp.c reports the attribute through report_attr().
  5. quote_string() preserves LF/CR in the attribute value.
  6. fprintf(stderr, "ATTR: %s=%s\n", ...) emits attacker-controlled newlines into the backend status stream.
  7. scheduler/statbuf.c parses the injected newline as a separate backend status line.
  8. The injected PPD: FoomaticRIPCommandLine=... line becomes CUPSD_LOG_PPD.
  9. scheduler/job.c parses the PPD update and keeps FoomaticRIPCommandLine because it is not in the CUPS-specific blacklist.
  10. On job completion, cupsdUpdatePrinterPPD() writes the key/value into /etc/cups/ppd/<queue>.ppd.
  11. A later print job loads the modified PPD.
  12. foomatic-rip reads FoomaticRIPCommandLine.
  13. The harmless marker command executes as user lp.

PoC

I validated this in an isolated Ubuntu 24.04 VM using a harmless marker command only.

Tested environment:

  • Ubuntu 24.04
  • CUPS 2.4.7
  • cups-browsed 2.0.0
  • cups-filters 2.0.0
  • libcupsfilters2t64 2.0.0
  • libppd2 2.0.0
  • foomatic-rip 2.0.0

The PoC is split into three scripts:

  • setup_cups_backend_status_foomatic_lab.sh: creates a temporary CUPS IPP queue with an initial safe Foomatic-backed PPD.
  • attack_cups_backend_status_foomatic_chain.py: starts a fake IPP printer, returns the crafted marker-message, submits two local print jobs, and checks the marker.
  • cleanup_cups_backend_status_foomatic_lab.sh: removes the temporary queue, lab files, and marker files where permitted.

Reproduction command pattern:

QUEUE=codex_backend_status_24_<id> PRINTER_HOST=127.0.0.1 PRINTER_PORT=18888 LAB_DIR=/tmp/codex-cups-backend-status-24-<id> sh poc/setup_cups_backend_status_foomatic_lab.sh

python3 poc/attack_cups_backend_status_foomatic_chain.py --listen-host 127.0.0.1 --listen-port 18888 --queue codex_backend_status_24_<id> --input /tmp/codex-cups-backend-status-24-<id>/input.ps --marker /tmp/codex-cups-backend-status-24-<id>-marker --timeout 24

Expected successful output:

marker exists: /tmp/codex-cups-backend-status-24-<id>-marker

uid=7(lp) gid=7(lp) groups=7(lp)

I also confirmed that the local queue PPD was modified:

*FoomaticRIPCommandLine: cat;/usr/bin/id>/tmp/codex-cups-backend-status-24-<id>-marker;/bin/chmod${IFS}0644${IFS}/tmp/codex-cups-backend-status-24-<id>-marker;#

The important security boundary is that the marker is written as user lp, showing execution through the CUPS filter path rather than a direct local shell command.

The PoC only writes harmless marker files under /tmp; it does not open a reverse shell, download payloads, or persist on the system.

Impact proposed by reporter

A malicious or compromised IPP printer may be able to modify the local PPD of a CUPS queue that points to it. If that queue uses a Foomatic-backed PPD/filter profile, the modified PPD can lead to command execution in the CUPS filter context as user lp.

This affects the trust boundary between a remote IPP printer and the local CUPS host. A printer status attribute should not be able to create scheduler control lines or modify local queue PPD command fields.

The impact is conditional:

  • The target must have an IPP/IPPS queue pointing to an attacker-controlled or compromised printer.
  • The queue must use a PPD/filter profile that invokes foomatic-rip.
  • A print job or monitoring/status path must consume the malicious printer status.
  • A later print job must trigger the modified Foomatic command line.

This is not demonstrated as default unauthenticated root RCE. It is validated as conditional RCE as user lp.

Impact explanation by OpenPrinting

AV is L (local) since the printer needs to be locally accessible from the system - you can't just send a packet to cupsd from any address.

Integrity is N (none) since nothing in CUPS will cause an integrity issue (foomatic is patched in cups-filters repo) - the PPD is affected by that doesn't affect cupsd or the system's integrity, just the (already bogus) queue which just makes it an availability issue.

Fix

[master 806e1b483] Sanitize IPP attribute strings.

[2.4.x d41cf0616] Sanitize IPP attribute strings.