Medium watch CVSS 8.1 chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
8.1
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Companion
DescriptionInappropriate implementation in Companion
ComponentCompanion
Bug ClassLogic Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker497828892
Fix commit6398ee594917 (chromium/src) +7/-0
CWECWE-693
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-03-30 (per vendor advisory)
Fix landed2026-04-01 (public git)
Disclosed2026-05-05
Patch gap34 days — fix public in source before the advisory

Background

CECA (Chrome Enterprise Companion App)
a helper that, per the commit, runs an out-of-process network fetcher primarily to authenticate with proxies over HTTPS to trusted endpoints.
Privilege dropping
when started as root, lowering to an unprivileged identity (“nobody”) before doing work, via setuid/setgid.
Supplementary groups
the additional POSIX group memberships a process carries beyond its primary gid, which setgid/setuid do not clear.
`initgroups`
the POSIX call that replaces the supplementary group list with the groups of a named user (here "nobody" with kNobodyGid).

Root Cause Analysis

When the network-worker process detected it was running as root (getuid() == 0), it dropped privileges by calling setgid(kNobodyGid) and (further down) setuid, but it never reset the supplementary group list. setgid/setuid change only the primary gid/uid and leave root’s inherited supplementary groups in place, so the “dropped” process still held the group memberships of root — an incomplete privilege drop.

The fix adds initgroups("nobody", kNobodyGid) before the setgid call, replacing the supplementary groups with only those of "nobody", and on failure logs via VPLOG, calls Shutdown(EnterpriseCompanionStatus::FromPosixErrno(errno)), and returns so the process does not continue with a partially-dropped identity. Ordering matters: initgroups requires privilege and so must run before setgid/setuid relinquish it, which the patch ensures. This is a hardening/defense-in-depth change to the privilege-drop sequence, not a fix to data parsing or a validation check.

Key insight
The core mistake was dropping uid/gid to “nobody” without clearing root’s inherited supplementary groups, leaving residual group privileges; adding initgroups("nobody", kNobodyGid) (with a fail-closed Shutdown/return) completes the privilege drop.

Attack Path

  1. Worker starts as root The CECA network fetcher process launches with getuid() == 0 and enters the privilege-drop block.
  2. Incomplete drop (pre-patch) It sets gid/uid to “nobody” but retains root’s supplementary group memberships, so the running process still belongs to privileged groups.
  3. Abuse residual group access Code execution in that process (inferred: would require a separate flaw, since the commit states CECA handles no untrusted data and talks only to trusted HTTPS endpoints) could reach group-owned resources it should no longer be able to access.
  4. Fix closes the gap With initgroups applied, the process carries only “nobody”’s groups, removing the residual privilege; a failing initgroups shuts the process down rather than proceeding.

Impact Assessment

The flaw leaves the network-worker process with root’s supplementary group memberships after an otherwise-successful uid/gid drop, weakening sandboxing/privilege-separation on POSIX systems; any concrete privilege misuse is (inferred) and would require a separate code-execution bug, which the commit notes is mitigated by CECA not handling untrusted data and using only trusted HTTPS endpoints. The advisory rates this medium severity (inappropriate implementation in Companion) and does not report in-the-wild exploitation. The precondition is the worker being started as root so the drop path runs.

Files Changed

  • chrome/enterprise_companion/app/app_net_worker.cc

Audit Directions

  • setuid/setgid without initgroups
    Audit every privilege-drop sequence for a missing initgroups/setgroups call that leaves supplementary groups intact.
  • Privilege-drop ordering
    Verify group-list resets occur while the process is still privileged (before setuid) and that each step fails closed rather than continuing on error.
  • Helper/worker process hardening
    Review out-of-process helpers that run or may run as root for complete identity transitions and, where available, additional sandboxing (e.g. seatbelt, as the commit suggests).
From 6398ee594917045f09ab227b3008ca54b92c71b1 Mon Sep 17 00:00:00 2001
From: Noah Rose Ledesma <[email protected]>
Date: Wed, 01 Apr 2026 13:02:16 -0700
Subject: [PATCH] Reset supplementary group list in CECA network fetcher

It is prudent to reset the supplementary group list for "nobody" in
addition to setting gid and uid to "nobody". A more robust solution
might be to use seatbelt for sandboxing.

Note that CECA utilizes an out-of-process network fetcher primarily
to authenticate with proxies. The application does not handle
untrusted data; communications occur exclusively over HTTPS with
trusted endponints.

Bug: 497828892
Change-Id: Ia0725266ab11714a8e30545429e13d486a6a6964
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7712334
Reviewed-by: Mark Mentovai <[email protected]>
Commit-Queue: Noah Rose Ledesma <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1608734}
---

diff --git a/chrome/enterprise_companion/app/app_net_worker.cc b/chrome/enterprise_companion/app/app_net_worker.cc
index 1def3ccc..a813b96 100644
--- a/chrome/enterprise_companion/app/app_net_worker.cc
+++ b/chrome/enterprise_companion/app/app_net_worker.cc
@@ -56,6 +56,13 @@
 
     // If running as root, drop down to "nobody".
     if (getuid() == 0) {
+      // Clear supplementary groups inherited from root.
+      if (initgroups("nobody", kNobodyGid) != 0) {
+        VPLOG(1) << "Failed to initgroups";
+        Shutdown(EnterpriseCompanionStatus::FromPosixErrno(errno));
+        return;
+      }
+
       if (setgid(kNobodyGid)) {
         VPLOG(1) << "Failed to set gid " << kNobodyGid;
         Shutdown(EnterpriseCompanionStatus::FromPosixErrno(errno));
Loading diff…

Original Bug Report

reported by [email protected]

Potential Local Privilege Escalation in CECA net worker via retained supplementary groups on macOS

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: The Chrome Enterprise Companion App (CECA) on macOS spawns a network worker that drops root privileges to the nobody user using setuid and setgid. However, it fails to clear supplementary groups, leaving the process with highly privileged admin and wheel group memberships. If the network worker is compromised, an attacker can leverage these retained groups to escalate to full root privileges.

Affected files:

  • chrome/enterprise_companion/app/app_net_worker.cc
  • chrome/enterprise_companion/url_loader_factory_provider.cc

Estimated timestamp from git blame: 2024-11-27

Summary

The Chrome Enterprise Companion App (CECA) on macOS utilizes an out-of-process network worker to isolate its networking tasks. When launched by the root LaunchDaemon (e.g., when no user is logged in), the worker correctly attempts to drop its privileges to the unprivileged nobody user. However, the implementation only calls setgid() and setuid(), which by POSIX standard do not alter a process’s supplementary group list. Consequently, the network worker retains root’s highly privileged supplementary groups, notably wheel (GID 0) and admin (GID 80).

Because this process handles untrusted network data without an additional Seatbelt sandbox, a compromise of the network stack allows an attacker to abuse the retained admin group membership to achieve Local Privilege Escalation (LPE) to root, entirely bypassing the intended UID isolation.

Vulnerability Details

In chrome/enterprise_companion/app/app_net_worker.cc, the FirstTaskRun method drops privileges as follows:

// If running as root, drop down to "nobody".
if (getuid() == 0) {
  if (setgid(kNobodyGid)) {
    // ... error handling ...
  }

  if (setuid(kNobodyUid)) {
    // ... error handling ...
  }
}

When a root process spawns a child, the child inherits its real/effective UID, GID, and supplementary groups. Calling setuid() and setgid() changes the primary IDs, but leaves the supplementary groups intact. Because there is no call to setgroups() or initgroups(), the process continues executing with admin and wheel group privileges.

Potential Attack Scenario

(Note: Our tooling currently lacks the ability to run code, so these are suggested/potential steps an attacker would follow based on architectural analysis.)

  1. Worker Launch: The CECA daemon (running as root) spawns the network worker process when no user is currently logged in. The child process inherits root’s supplementary groups.
  2. Incomplete Privilege Drop: The worker executes AppNetWorker::FirstTaskRun, changing its UID and GID to nobody (-2), but retains wheel and admin in its supplementary groups list.
  3. Network Stack Compromise: The worker begins processing untrusted data from the enterprise Device Management server or telemetry endpoints. An attacker exploits a vulnerability (e.g., memory corruption in //net) via a malicious server response or MitM attack.
  4. Code Execution: The attacker achieves Remote Code Execution (RCE) within the network worker. Their shellcode runs as nobody but holds admin group privileges.
  5. Escalation to Root: On macOS, the admin group has write access to sensitive system directories. The attacker writes a malicious CoreAudio plugin to /Library/Audio/Plug-Ins/HAL/.
  6. Triggering Root Execution: The attacker triggers a system audio event or restarts the coreaudiod service (which runs as root). coreaudiod loads the malicious plugin, executing the attacker’s payload with full root privileges.

Suggested Fix

Update AppNetWorker::FirstTaskRun to clear the supplementary groups before dropping the UID. You can achieve this by calling setgroups(0, NULL) to remove all supplementary groups, or initgroups("nobody", kNobodyGid) to set them to the default for the nobody user.

if (getuid() == 0) {
  // Clear supplementary groups inherited from root.
  if (setgroups(0, nullptr) != 0) {
    VPLOG(1) << "Failed to clear supplementary groups";
    Shutdown(EnterpriseCompanionStatus::FromPosixErrno(errno));
    return;
  }

  if (setgid(kNobodyGid)) {
    // ...

Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0


Results from so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker