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:
- No
O_NOFOLLOW — if tempfile is a symlink, open() follows it and
operates on the link target.
- No
O_EXCL — open() happily reuses a pre-existing path instead of
failing, so a planted symlink (or file) is accepted.
- Predictable name in a group-writable directory —
con->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
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.c — TempDir created 01770 root:lp.
- CWE-59, CWE-377.
Attachments
poc.sh
Summary
When a printer is added or modified with a model PPD, the cupsd scheduler
(running as root) calls
copy_model()inscheduler/ipp.c, which writes thedriver-generated PPD to a temporary file. The tempfile path is fully predictable
(
<TempDir>/<con->number>.ppd, wherecon->numberis the sequential clientconnection id) and is opened with
open(tempfile, O_WRONLY | O_CREAT | O_TRUNC, 0600)— withoutO_EXCLand withoutO_NOFOLLOW.TempDir(
/var/spool/cups/tmp) is mode01770 root:lp, i.e. writable by thelpgroupthat print filters run under. An
lp-group process can pre-plant a symlink at thepredicted 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.copens temporaries safely withO_RDWR|O_CREAT|O_EXCL|O_NOFOLLOW;copy_model()is the inconsistent outlier.Severity
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 thecreate/symlink race and predicting
con->number; I/A:H = arbitraryroot-owned-file truncation/overwrite.)
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
dc9dea0)scheduler/ipp.ccopy_model()open)Vulnerability Details
Root Cause
scheduler/ipp.c:4356-4357(cupsd runs as root):Three properties combine into an exploitable TOCTOU:
O_NOFOLLOW— iftempfileis a symlink,open()follows it andoperates on the link target.
O_EXCL—open()happily reuses a pre-existing path instead offailing, so a planted symlink (or file) is accepted.
con->numberis themonotonic client connection counter (easily predicted/forced), and
TempDiris
01770 root:lp(scheduler/conf.c), writable by grouplp.The same source tree demonstrates the correct pattern in
cups/tempfile.c:114:copy_model()does not use it.Reachability
copy_model()is invoked from the CUPS-Add/Modify-Printer path (admin-gated toadd 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 arealistic foothold (e.g. via the fax-option injection issue, or any filter bug) —
pre-creates
TempDir/<N>.ppdas a symlink to a root-owned target before/at themoment cupsd opens it. Root's
O_TRUNCthen truncates the target, and the PPDbytes are written into it.
Proof of Concept
Necessary Scripts
poc.sh— reproduces the exact unsafeopen(O_WRONLY|O_CREAT|O_TRUNC, 0600)flags from
ipp.c:4357in a small C tool, plants a symlink at the predictedtempfile 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
Expected (correct) Output
The privileged
open()should refuse to follow a pre-planted symlink (and/orfail because the path already exists), leaving the victim file untouched — the
behavior of the
O_EXCL|O_NOFOLLOWvariant.Actual Output (vulnerable)
Manual Verification
Confirm in source: predictable name + unsafe flags at
ipp.c:4356-4357; thegroup-writable
01770 root:lpTempDirinscheduler/conf.c; and the safecounter-example in
cups/tempfile.c:114.Output Analysis
O_WRONLY|O_CREAT|O_TRUNC(copy_model)O_WRONLY|O_CREAT|O_TRUNC|O_EXCL|O_NOFOLLOW(fix)EEXIST)Impact
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.
lp-group membership (CUPS filters) ableto 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 safeO_EXCL|O_NOFOLLOWpattern in the same tree.scheduler/conf.c—TempDircreated01770 root:lp.Attachments
poc.sh