CWE-272: Least Privilege Violation
Overview
A least privilege violation occurs when a process acquires an elevated privilege to perform one specific operation - binding to a low network port, calling chroot(), wrapping a privileged API call - and then keeps running with that privilege instead of dropping it immediately once the operation finishes. The elevated access should exist only for the instant it's needed; leaving it active afterward means any later vulnerability in the same process runs with far more power than the task ever required.
Relationship to Other CWEs
- CWE-272 (this page) - privilege acquired for one specific operation and still held after that operation finishes, instead of being dropped immediately.
- CWE-271 (Privilege Dropping / Lowering Errors) - the parent class, with no page here yet, covering every way a process fails to give up privilege it no longer needs. CWE-271 in turn sits under CWE-269 (Improper Privilege Management), which is the page to read for the privilege lifecycle as a whole.
- CWE-273 (Improper Check for Dropped Privileges) - a sibling, also ChildOf CWE-271, and the other half of the same fix. CWE-272 is about performing the drop immediately after the privileged operation; CWE-273 is about verifying the drop succeeded rather than assuming it did. See CWE-273 for the return-value-checking detail.
- CWE-250 (Execution with Unnecessary Privileges) - overlaps with this page by MITRE's own account: its relationship note on CWE-272 says "CWE-271, CWE-272, and CWE-250 are all closely related and possibly overlapping." Treat the split below as a reporting preference, not a test that always decides. CWE-250 covers the standing condition: a service, container, or database connection configured with more privilege than it ever needs, for its entire lifetime, such as a root-owned web server, a DBA-level application account, or a root container. CWE-272 covers privilege that was legitimately needed for one operation and was not given up afterward. A process that runs as root at all times with no privileged operation to point to is clearly CWE-250; a
chroot()with no followingsetuid()is clearly CWE-272; the common case of a daemon that binds a low port at startup and never drops sits squarely in the overlap, and MITRE's own demonstrative examples for the two are drawn from it. Either citation is defensible there and the remediation is the same either way, so do not re-triage a finding from one number to the other unless something else depends on which it is.
Risk
High: privilege that outlives the operation it was needed for widens the window during which any other vulnerability in the same process - a memory corruption bug, an injection flaw, a logic error - executes with that same elevated access, turning what could have been a contained bug into a full privilege-escalation path.
Remediation Steps
Core Principle: acquire elevated privilege for the narrowest possible window, drop it permanently the instant the privileged operation completes, and verify the drop succeeded before doing anything else.
Trace the Data Path
- Source: a startup or request path that needs one specific privileged capability (binding a low port, restricting a filesystem root, an admin API call)
- Sink: all the code that runs after the privileged operation, still executing under the elevated identity because nothing dropped it
- Missing control: no privilege-drop call immediately following the operation, or a drop that's incomplete (effective identity only, with supplementary groups or capabilities left attached)
Drop Privileges Immediately After Use (Primary Defense)
// SECURE - acquire, use, drop immediately, verify
require_elevated_privilege()
result = perform_privileged_operation() // e.g. bind to port 80, chroot()
drop_privileges_permanently(to=unprivileged_identity)
if still_has_elevated_privilege():
fail_immediately("privilege drop did not take effect")
continue_with(result) // everything from here runs unprivileged
- Drop every component of the elevated identity together - effective ID, saved ID, supplementary groups, and any OS capabilities - not just the most visible one
- Get the order right: on POSIX, clear the supplementary groups and set the group ID before the user ID.
setuidgives away the very privilegesetgroupsandsetgidrequire, so calling it first leaves those later calls failing while the process still carries every group the privileged identity belonged to - Perform the drop before handling any untrusted input, not after
- Attempt to re-acquire the dropped privilege as a verification step; it should fail - see CWE-273 for the full check-the-return-value pattern
Avoid Needing the Privilege At All (Secondary Defense)
Where the platform supports it, replace the privileged operation with a narrower mechanism that never requires full elevation in the first place - for example, an OS capability scoped to exactly one action (such as binding to a low port) instead of running as root to get the same result. This removes the drop step's failure mode entirely, at the cost of being platform-specific.
Test the Remediation
- After the code path that should have dropped privilege completes, attempt the privileged operation again and confirm it fails
- Confirm the process's effective and saved identity both reflect the unprivileged account, not just the real one
- Re-scan with a security tool to confirm the finding is resolved
Common Vulnerable Patterns
// VULNERABLE - privilege acquired, used, and never dropped
require_elevated_privilege()
result = perform_privileged_operation()
handle_requests(result) // still running with full elevated privilege!
Why this is vulnerable: the elevated privilege was needed for one call and is still held for all of them. handle_requests is where untrusted input arrives, so the code most exposed to attack is running with the most authority, and all that separates the two is one missing line.
Dropping privilege is easy to do partially, which is why the drop needs checking rather than assuming. Changing the effective identity while leaving the saved one intact lets the process raise its privilege again, so an attacker who reaches code execution can restore what was dropped. Identity is also not the whole of privilege on modern systems: capabilities, supplementary groups, and an open handle obtained earlier all survive a change of user. A drop that has not been verified from the process's own reported state is a drop that may not have happened.
Secure Patterns
// SECURE - privilege dropped immediately after the operation it was needed for
require_elevated_privilege()
result = perform_privileged_operation()
drop_privileges_permanently(to=unprivileged_identity)
handle_requests(result) // runs unprivileged from here on
Why this works: the elevated identity exists only for the single operation that required it. Any vulnerability reachable from handle_requests afterward executes with the unprivileged identity, containing the blast radius instead of handing an attacker root or admin access for free.
Common Pitfalls
- Dropping the effective identity but not the saved one: on POSIX systems, calling
seteuid()alone changes what the process currently acts as but leaves the saved user ID at the original privileged value - a later call can silently re-acquire it. The drop needs to be permanent (setuid, notseteuid), not just temporary. - Only dropping on the success path: the drop is skipped when an exception, error, or early return happens first - the process is left elevated with no drop scheduled at all.
- Dropping identity but not capabilities or supplementary groups: changing the user ID while leaving Linux capabilities or group memberships from the privileged identity attached - the process is no longer "root" by name but still has root-equivalent access through what it retained. Dropping them in the wrong order produces the same result from code that looks complete: once the user ID is gone, the group calls that follow it have no privilege left to succeed with, so an unchecked
setgroups/setgidaftersetuidsilently leaves the groups in place. - Assuming the drop succeeded without checking: treating a call to a privilege-dropping function as unconditionally successful - this is CWE-273's exact failure mode; a silently failed drop leaves the process running elevated while the rest of the code proceeds as if it were not.