Skip to content

CUPS copy_model() creates a predictable PPD tempfile without O_EXCL/O_NOFOLLOW (symlink TOCTOU, lp → root)

Moderate
michaelrsweet published GHSA-jj94-x3qh-ffp9 Jun 17, 2026

Package

https://github.com/OpenPrinting/cups

Affected versions

<= 2.4.19

Patched versions

future 2.4.20

Description

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


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 f6a11da] Open temporary PPD files more securely.

[2.4.x 98adfd8] 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

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Local
Attack complexity
High
Privileges required
High
User interaction
None
Scope
Unchanged
Confidentiality
None
Integrity
High
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:H/A:H

CVE ID

CVE-2026-55480

Weaknesses

No CWEs

Credits