high · 7.9CVE-2026-54552Jul 17, 2026

CVE-2026-54552: sh Incomplete Privilege Drop via _uid (Supplementary Groups Not Cleared)

Rohit Hatagale
AI Security Researcher, SecureLayer7

The Python sh library's _uid option changed a subprocess's user ID but forgot to clear the parent process's supplementary groups, so a child process expected to run with reduced privileges could still

Packagesh
Ecosystempip
Affected< 2.2.4
Fixed in2.2.4

The problem

The `_uid` special argument in the `sh` library is meant to launch a subprocess as a less-privileged user. On Linux, dropping privileges correctly requires three calls in order: `setgroups([])` to clear supplementary groups, then `setgid()`, then `setuid()`.

Versions before 2.2.4 performed `setuid()` and `setgid()` but skipped `setgroups([])`. A child process spawned from a root parent with `_uid=someuser` would still carry root's full supplementary group list, including groups like `docker`, `disk`, `shadow`, or `sudo`.

Any file or resource gated on those groups remained accessible to the supposedly sandboxed subprocess.

Proof of concept

A working proof-of-concept for CVE-2026-54552 in sh, with the exact payload below.

python
import os
import sh

# Run as root. Spawn a subprocess as an unprivileged uid.
# Before the fix, the child retains root's supplementary groups.
result = sh.python3(
    "-c",
    "import os; print('uid:', os.getuid(), 'groups:', os.getgroups())",
    _uid=65534  # nobody
)
print(result)
# Vulnerable output example:
# uid: 65534 groups: [0, 4, 24, 27, 30, 46, 117]  <-- root's groups still present
#
# Fixed output (2.2.4+):
# uid: 65534 groups: []  <-- supplementary groups cleared

The root cause is CWE-271 (Improper Reduction of Privileges). POSIX requires `setgroups()` to be called before `setuid()`/`setgid()` to fully relinquish a privileged process's group memberships. The `sh` library's pre-fork callback that applied `_uid` called `os.setgid()` and `os.setuid()` but had no `os.setgroups([])` call.

The patch for 2.2.4 adds `os.setgroups([])` as the first step inside the privilege-drop code path, clearing all supplementary groups before switching uid/gid. This mirrors the fix applied to Python's own subprocess module for CVE-2023-6507, which had the identical omission with `extra_groups=[]`.

The fix

Upgrade the `sh` package to version 2.2.4 or later (`pip install --upgrade 'sh>=2.2.4'`). As a workaround on older versions, avoid using `_uid` to enforce a privilege boundary from a privileged parent; use a separate process that starts unprivileged instead.

Reporter not attributed.

References: [1][2][3]

Related research