CVE-2026-63349: anyio Incorrect Privilege Assignment in run_process / open_process
AnyIO 4.14.0 and 4.14.1 silently ignore the extra_groups argument when spawning child processes, so callers cannot reliably drop supplementary POSIX groups from less-privileged subprocesses.
The problem
AnyIO 4.14.0 introduced POSIX extra_groups support in open_process() and run_process(), but the implementation contains a variable-name bug: the backend receives the value of group instead of extra_groups.
This means passing extra_groups=[] does not call setgroups() on the child, leaving the parent's supplementary groups in place. If group and extra_groups are both supplied, the backend receives an integer where a list is expected and raises TypeError. Applications that rely on these APIs to drop privileges before exec() are silently left exposed.
Proof of concept
A working proof-of-concept for CVE-2026-63349 in anyio, with the exact payload below.
import asyncio
import anyio
import os
# Simulate a parent process carrying a synthetic supplementary group (e.g. gid 1337).
# On anyio 4.14.0 / 4.14.1, extra_groups=[] is silently ignored:
# the child inherits the parent's full supplementary group list.
async def demonstrate_bypass():
# Expected: child has NO supplementary groups (empty list passed).
# Actual on vulnerable versions: child retains parent supplementary groups.
result = await anyio.run_process(
["python3", "-c", "import os; print(os.getgroups())"],
extra_groups=[], # should clear supplementary groups -- does NOT
)
print("Child groups (should be []):", result.stdout.decode().strip())
# Extra: combining group= and extra_groups= raises TypeError on vulnerable builds
# because the backend receives an int where a list is expected.
try:
await anyio.run_process(
["id"],
group=1000,
extra_groups=[],
)
except TypeError as e:
print("TypeError (group int passed as extra_groups):", e)
asyncio.run(demonstrate_bypass())The root cause is a one-character variable-name mistake in the anyio asyncio backend's open_process() helper. When extra_groups is not None, the code wrote kwargs["extra_groups"] = group (the GID integer) instead of kwargs["extra_groups"] = extra_groups (the caller-supplied list).
This maps to CWE-266 (Incorrect Privilege Assignment) and CWE-272 (Least Privilege Violation): the privilege-dropping intent of the caller is completely discarded before exec().
The patch (commit eb562e6, PR #1209) corrects the variable name so extra_groups is forwarded as supplied. An empty list now reaches setgroups(0, NULL) in the kernel, actually clearing supplementary groups on the child.
The fix
Upgrade anyio to 4.14.2 or later. The single-line fix is in the asyncio subprocess backend: change kwargs["extra_groups"] = group to kwargs["extra_groups"] = extra_groups. No API or configuration changes are needed by callers.
Related research
- criticalCVE-2026-63374CVE-2026-63374: anyio TLS Certificate Spoofing via IDNA 2003 Hostname Encoding
- critical · 9.8motionEye LFI to Unauthenticated RCE Chain (CVSS 9.8)
- high · 7.5LMDeploy SSRF Bypass via urlparse and requests Parser Disagreement
- critical · 9.1CVE-2026-59163CVE-2026-59163: mnemosyne-memory JWT Signature Verification Bypass