VNX-272: Least Privilege Violation
Detects root privilege kept after the privileged operation that needed it: a chroot in a project that never drops its identity afterwards, and a return to root (seteuid(0), setuid(0) in a setuid helper, os.seteuid(0)) that is never lowered again. C, C++, Python and Go.
| Field | Value |
|---|---|
| Rule ID | VNX-272 |
| Severity | Medium |
| SARIF level | warning |
| Kind | sast |
| Languages | C, C++, Python, Go |
| CWE | CWE-272 |
| CAPEC | CAPEC-66 |
| ATT&CK | T1190 |
| CWSS vector | CWSS:1.0/TI:M/AP:RU/AL:S/IC:N/FC:LT/RP:RU/RL:S/AV:L/AS:N/IN:A/SC:A/BI:M/DI:H/EX:M/EC:N/P:C |
| Tags | least-privilege chroot privilege-drop cwe-272 |
| Source | internal/sast/rules/vnx-272.rego |
Overview
VNX-272 maps to CWE-272: Least Privilege Violation. Some operations need root: chroot(), binding a port below 1024, opening a protected device. The privilege should be given up as soon as that operation is done. This rule reports two concrete ways it is not:
- A chroot with no privilege drop. The file calls
chroot()(C/C++),os.chroot(), achroot()imported fromosor called on animport os asalias (Python),syscall.Chroot()/unix.Chroot()or setsSysProcAttr{Chroot: ...}(Go, inline or on its own line), and nothing in the project’s code for that language ever changes identity: nosetuid/setgid/setresuid/setgroups/initgroups, nodrop_privileges()/drop_root()/lower_euid()/priv_drop()/switch_user()-style wrapper, noSysProcAttr.Credential(in the literal or assigned afterwards). A process that is still root inside a jail can leave it, and every file it opens is opened as root. - An elevation that is never lowered.
seteuid(0),setegid(0),setuid(0)/setgid(0)(the classic setuid-root wrapper that then runssystem()),setreuid(-1, 0),setresuid(-1, 0, -1)oros.seteuid(0)/os.setuid(0)with no later call in the file that sets the effective identity back, directly or through such a wrapper.
What Gets Flagged
// FLAGGED: still root after entering the jail
chroot(APP_HOME);
chdir("/");
FILE *data = fopen(argv[1], "r+");
# FLAGGED: raised to root, never lowered
def read_shadow_entry(user):
os.seteuid(0)
with open("/etc/shadow") as shadow:
...
// FLAGGED: the child is chrooted but runs as root
cmd.SysProcAttr = &syscall.SysProcAttr{Chroot: rootfs, Setpgid: true}
What Is Not Flagged
- A chroot followed by
setgroups/setgid/setuid(oros.setgid/os.setuid, aCredentialinSysProcAttr), including when the drop lives in another file of the project, such as a privsep module. seteuid(0)followed later byseteuid(saved)or adrop_root()-style call (afinally:block counts), and an elevation inside a function whose name says it regains privileges (privs_regain(),restore_root()): its caller lowers them.- A check that root can no longer be regained after a drop:
if (setuid(0) != -1) abort();,if (!seteuid(0)), or a Pythontry: os.seteuid(0)whoseelse:raises. - A
Chroot:field of a struct that is notSysProcAttr, such as an SFTP server’s configuration. - Test code, member calls such as
ops->chroot(dir), and the names in comments or string literals.
Whether a privileged section is longer than it needs to be cannot be decided line by line and is not judged.
Remediation
Drop supplementary groups, then the group, then the user, immediately after the privileged step, and check every call:
if (chroot(dir) != 0 || chdir("/") != 0)
return -1;
if (setgroups(0, NULL) != 0 || setgid(pw->pw_gid) != 0 || setuid(pw->pw_uid) != 0)
return -1;
For a temporary elevation, restore the saved identity on every path:
unprivileged = os.geteuid()
os.seteuid(0)
try:
return open(path).read()
finally:
os.seteuid(unprivileged)
References
Suppressing this rule
Put a nosec comment on the line, or on line 1 to cover the whole file. Use the comment marker of the file’s language (#, //, -- or ;):
# nosec vnx-272
// nosec vnx-272
Or record an ignore rule: vulnetix ignore add --rule vnx-272 --reason "...". See Suppressing findings.