Skip to content

GHSA-jj94-x3qh-ffp9

CVE Information

Summary

When a printer is added or modified with a model PPD, the cupsd scheduler (running as root) calls copy_model() in scheduler/ipp.c, which writes the driver-generated PPD to a temporary file. The tempfile path is fully predictable (<TempDir>/<con->number>.ppd, where con->number is the sequential client connection id) and is opened with open(tempfile, O_WRONLY | O_CREAT | O_TRUNC, 0600)without O_EXCL and without O_NOFOLLOW. TempDir (/var/spool/cups/tmp) is mode 01770 root:lp, i.e. writable by the lp group that print filters run under. An lp-group process can pre-plant a symlink at the predicted path pointing at any root-owned file; root follows the symlink and truncates/overwrites the target, giving an lp → root arbitrary-file-write primitive. The codebase's own cups/tempfile.c opens temporaries safely with O_RDWR|O_CREAT|O_EXCL|O_NOFOLLOW; copy_model() is the inconsistent outlier.


Severity

  • CVSS: 5.7 (Medium) — CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:H/A:H (PR:L = an lp-group foothold, e.g. a print filter; AC:H = winning the create/symlink race and predicting con->number; I/A:H = arbitrary root-owned-file truncation/overwrite.)
  • CWE: CWE-59 (Improper Link Resolution Before File Access — symlink following) / CWE-377 (Insecure Temporary File).

Reviewed by Mike Sweet, OpenPrinting:

Rescored privileges - they are high since you need to be root/print admin to install the malicious filter in the first place.


Affected Version

  • Package: https://github.com/OpenPrinting/cups
  • Version: 2.5.0 (HEAD dc9dea0)
  • File: scheduler/ipp.c
  • Function: copy_model()
  • Lines: 4356–4357 (tempfile name + unsafe open)

Vulnerability Details

Root Cause

scheduler/ipp.c:4356-4357 (cupsd runs as root):

snprintf(buffer,   sizeof(buffer),   "%s/daemon/cups-driverd", ServerBin);
snprintf(tempfile, sizeof(tempfile), "%s/%d.ppd", TempDir, con->number);
if ((tempfd = open(tempfile, O_WRONLY | O_CREAT | O_TRUNC, 0600)) < 0)
    return (-1);

Three properties combine into an exploitable TOCTOU:

  1. No O_NOFOLLOW — if tempfile is a symlink, open() follows it and operates on the link target.
  2. No O_EXCLopen() happily reuses a pre-existing path instead of failing, so a planted symlink (or file) is accepted.
  3. Predictable name in a group-writable directorycon->number is the monotonic client connection counter (easily predicted/forced), and TempDir is 01770 root:lp (scheduler/conf.c), writable by group lp.

The same source tree demonstrates the correct pattern in cups/tempfile.c:114:

fd = open(tmpdir_path, O_RDWR | O_CREAT | O_EXCL | O_NOFOLLOW, 0600);

copy_model() does not use it.

Reachability

copy_model() is invoked from the CUPS-Add/Modify-Printer path (admin-gated to add the printer), but the file-write primitive itself is the race: a process in group lp — the privilege level every CUPS filter already runs at, and a realistic foothold (e.g. via the fax-option injection issue, or any filter bug) — pre-creates TempDir/<N>.ppd as a symlink to a root-owned target before/at the moment cupsd opens it. Root's O_TRUNC then truncates the target, and the PPD bytes are written into it.


Proof of Concept

Necessary Scripts

  • poc.sh — reproduces the exact unsafe open(O_WRONLY|O_CREAT|O_TRUNC, 0600) flags from ipp.c:4357 in a small C tool, plants a symlink at the predicted tempfile name pointing at a stand-in "root victim" file, and shows the victim overwritten through the symlink. It then runs the same path with the fixed flags (O_EXCL|O_NOFOLLOW) to show they reject the planted symlink.

Steps to Reproduce

./poc.sh

Expected (correct) Output

The privileged open() should refuse to follow a pre-planted symlink (and/or fail because the path already exists), leaving the victim file untouched — the behavior of the O_EXCL|O_NOFOLLOW variant.

Actual Output (vulnerable)

[*] attacker planted symlink: <spool>/42.ppd -> <victim_root_file>
[*] cupsd opens <spool>/42.ppd with O_WRONLY|O_CREAT|O_TRUNC (no O_EXCL/O_NOFOLLOW)
[*] victim after : PWNED-BY-cupsd-following-attacker-symlink
[CONFIRMED] symlink followed -> arbitrary root-owned file overwritten.
[*] same path with the fix (O_EXCL|O_NOFOLLOW, as cups/tempfile.c:114 uses):
safe open (rejected as expected): File exists

Manual Verification

Confirm in source: predictable name + unsafe flags at ipp.c:4356-4357; the group-writable 01770 root:lp TempDir in scheduler/conf.c; and the safe counter-example in cups/tempfile.c:114.

Output Analysis

Open flags Pre-planted symlink Result
O_WRONLY\|O_CREAT\|O_TRUNC (copy_model) followed root truncates/writes the symlink target
O_WRONLY\|O_CREAT\|O_TRUNC\|O_EXCL\|O_NOFOLLOW (fix) rejected (EEXIST) victim untouched

Impact

  • Integrity: Arbitrary truncation/overwrite of any root-writable file as the cupsd root process (the PPD content is written into the symlink target). Chained to a config or credential file, this is a clean lp → root escalation.
  • Availability: Truncation of critical root-owned files.
  • Confidentiality: Not directly (write primitive).
  • Attack vector: Local process with lp-group membership (CUPS filters) able to win the create race on the predictable tempfile name.

Fix

[master f6a11dae0] Open temporary PPD files more securely.

[2.4.x 98adfd855] Open temporary PPD files more securely.


References

  • scheduler/ipp.c (OpenPrinting CUPS 2.5.0): copy_model(), lines 4356-4357.
  • cups/tempfile.c:114 — the safe O_EXCL|O_NOFOLLOW pattern in the same tree.
  • scheduler/conf.cTempDir created 01770 root:lp.
  • CWE-59, CWE-377.

Attachments

poc.sh