CWE-708: Incorrect Ownership Assignment
Overview
Incorrect ownership assignment happens when a file, directory, process, or other resource is assigned an owner outside the application's intended control sphere. That principal can then read or modify the resource directly, rather than only through the intended, access-controlled path. It commonly appears in installers that create files with the wrong user/group, ownership changes that land on a symlink's target instead of the symlink itself, and session teardown that forgets to hand a resource's ownership back to its rightful owner.
Relationship to Other CWEs
CWE-708 is a child of CWE-282 (Improper Ownership Management), the broader weakness of failing to manage resource ownership correctly. It can also manifest as a form of CWE-345 (Insufficient Verification of Data Authenticity) when the ownership decision trusts an unverified claim about who a resource belongs to.
Ownership and permissions are two separate checks on the same resource, and this page is only the first of them. CWE-732 (Incorrect Permission Assignment for Critical Resource) is the mode or ACL being wrong rather than the owner. The two land on the same file often enough that both are worth checking together, and fixing one does not fix the other: a file owned by exactly the right account is still readable by everyone if its mode says so.
OWASP Classification
A01:2025 - Broken Access Control
Risk
High: A resource owned by the wrong principal is readable and writable by that principal outside any access-control check the application performs. This has produced privilege escalation (CVE-2011-1551: a sensitive directory tree assigned to a user account, enabling privileged operations the user should never have had) and data tampering via ownership-following symlink races (CVE-2005-1064, CVE-2005-3148).
Remediation Steps
Core Principle: Assign resource ownership only to the specific principal the application intends, name the target of an ownership change by an open handle rather than by a path a symlink can re-point, and never let a grant outlive the session that needed it.
Trace the Data Path
- Source: Code that creates a resource (an installer, a file-upload handler, a provisioning script) or that changes ownership as part of session/privilege management.
- Sink: The ownership-assignment call itself (a chown-style syscall, an ACL owner field, a cloud resource's owner/account metadata).
- Data Flow / Missing Controls: No check that the ownership target is the intended principal, and no guarantee that the object the call lands on is the one the caller meant. Every call that takes a path resolves it afresh, so a symlink dropped in between sends the change somewhere else.
Assign Ownership Explicitly to the Intended Principal (Primary Defense)
- Set ownership at creation time to a specific, intended principal. Do not rely on an ambient default such as the process's current user or the invoking user without confirming that default is correct for this resource.
- When changing ownership of an existing path, open it once with symlink-following disabled and apply the change to the resulting handle. Do not inspect the path and then change it by name: those are two separate resolutions, and the attacker only has to swap the name between them.
- For installers and provisioning code, pin the owning user/group explicitly for every installed file rather than inheriting whatever the build or packaging environment happened to use.
Restore Ownership on Session and Privilege Boundaries
- When a resource's ownership is temporarily changed for the duration of a session or elevated operation (a device, a temp directory, a lock file), restore the original owner when that session ends or the operation completes. A forgotten restore leaves the resource owned by whoever used it last.
- Treat ownership restoration as part of the same transaction as the privilege drop or logout, not a best-effort cleanup step that can be skipped on an error path.
- Add a restore path that does not depend on the granting process still being alive: reconcile each shared resource's owner against its configured owner at startup, or have the supervisor perform the restore when the session's process exits. An in-process
finallycannot run after aSIGKILL, an OOM kill or a power loss, and those are exactly the cases that leave a grant standing indefinitely.
Verify Ownership After Assignment (Defense in Depth)
- Read back the resource's actual owner after setting it and confirm it matches what was intended, rather than assuming the operation succeeded.
- For high-value resources, add periodic auditing that flags any resource whose owner no longer matches the expected principal.
Test the Fix
- Create a symlink pointing at a victim file, then trigger the ownership-change operation against the symlink's path. The operation must act on the symlink itself or refuse, never silently retarget the victim file's ownership.
- Complete a session or privileged operation that temporarily takes ownership of a shared resource, then confirm ownership is restored to the original principal afterward.
- Kill the granting process outright mid-session (
SIGKILL, not a graceful stop), then restart the service: the resource's owner must be the configured original, not the session's user. A test that only ends sessions gracefully passes against afinallyalone and says nothing about the ending that actually leaves a grant standing. - Attempt to trigger the ownership-assignment code with an unexpected or attacker-influenced target identity and confirm it is rejected rather than applied.
- Re-scan with the security scanner to confirm the finding is resolved.
Common Vulnerable Patterns
An ownership change that follows a symlink
// VULNERABLE - follows a symlink when changing ownership
path = user_supplied_path
set_owner(path, target_user)
// Attack: path is a symlink to a victim's file; the victim's file gets
// reassigned to target_user, not the symlink itself
Why this is vulnerable: the operation resolves the path and acts on what it finds at the end of it, so the object whose ownership changes is chosen by whoever controls the link rather than by the caller. Since this runs privileged - an unprivileged process cannot give a file away - the attacker borrows that privilege to reassign a file they could not otherwise touch, and the target need not be theirs or even readable to them.
Checking the path first does not fix it, and is the change most likely to be proposed. Between the check and the operation the attacker replaces the path with a symlink, so the inspection and the action apply to two different objects; the window is small and can be widened arbitrarily by making the process wait. What closes it is never resolving the link at all: the variant of the call that acts on the link itself, or opening the file once and operating on the resulting handle so the object cannot be substituted underneath. See CWE-367 for the general shape.
Ownership granted for a session and never returned
// VULNERABLE - ownership never restored after a privileged session ends
acquire_device(device, current_user)
// ... session runs, device temporarily owned by current_user ...
// session ends without restoring the device's original owner
Why this is vulnerable: the grant was correct and temporary, and only the first of those is implemented. The user genuinely should own the device while their session runs; what is missing is the counterpart that takes it back, so the permission outlives the justification for it and the previous user keeps access after the next one arrives.
The absence has no symptom, which is why it persists. Everything works: the current session works because the ownership was granted, and the following session usually works too, because it grants ownership again over a resource nobody checked the prior state of. The defect only shows up as the earlier user still being able to reach the device - which nobody is testing for. Anything that hands out ownership has to be paired with a restore in a construct that runs on every exit path it can reach, including the ones that throw. The restore should return the resource to a known original rather than to whatever it happens to have been. That still leaves the exit paths no in-process construct can reach: a SIGKILL, an OOM kill, a container stopped, a power loss. None of those runs a finally, so a grant that must not survive the session needs a second mechanism outside the process - reconciling ownership against the intended owner at startup, or a supervisor that performs the restore when the session's process goes away.
Secure Patterns
// SECURE - open once without following links, then act on the handle
handle = open_no_follow(user_supplied_path) // refuses if the final component
// is a symlink (POSIX: ELOOP)
if handle == FAILED:
reject(describe_open_failure()) // a symlink refusal and a missing
// or unreadable path are different
// errors - report which one
try:
set_owner_on_handle(handle, target_user) // the object cannot be swapped
// out from under an open handle
finally:
close(handle)
// SECURE - ownership restoration is part of the same teardown as the session
acquire_device(device, current_user)
try:
// ... session runs ...
finally:
set_owner(device, original_owner) // restored on every exit this process runs -
// a SIGKILL skips it, see the note below
Why this works: In the first block the path is resolved exactly once, by the open, and the ownership change names the handle rather than the path. There is no second resolution for an attacker to redirect between the check and the change. A path-based check followed by a path-based set_owner would not achieve this: those are two resolutions of the same name, and swapping a symlink into place between them is the whole attack. Refusing to follow the final component is what makes the open safe to perform at all; on POSIX that is open() with O_NOFOLLOW followed by fchown(). O_NOFOLLOW guards only the last component, so a path whose parent directories an attacker can write to still needs those components resolved safely - openat() walking one component at a time, or a resolve-no-symlinks flag where the platform offers one. The second block changes ownership by path because a device node at a fixed, administrator-controlled location is not a path an attacker can re-point; where the path is attacker-influenced, use the handle form there too. Restoring ownership inside a finally-equivalent block returns the resource to its rightful owner on every exit the process itself survives to run: a thrown exception, an early return, a normal end of session. That closes the ordinary case of the grant outliving the session. It is not a guarantee against everything, because a finally does not run when the process is killed outright, so pair it with a reconciliation pass at startup that resets any resource whose owner is not the configured one.
Common Pitfalls
- Checking the path, then changing it by name:
is_symlink(path)followed byset_owner(path, ...)reads as a fix and is the same bug with an extra step, for the reason given above. - Reading a successful
lchownas "the file was chowned": switching fromchowntolchownis an improvement - it never follows the link, so the privileged change can no longer be redirected onto a victim's file, and on an ordinary path it behaves exactly likechown. What it does not do is perform the intended change when the path is a link: it reassigns the symlink itself and returns success, so code that treats that as "the target now belongs to the right user" carries on with a false belief. Refusing the operation, rather than quietly retargeting it at the link, is what makes the outcome legible. - Creating as one identity and correcting afterwards: installing or writing a file as root and calling
chownto hand it to the service account leaves a window in which the file exists under the wrong owner, and requires the privilegedchownthis weakness is about. Creating the resource as the identity that should own it removes both. - Restoring to "whatever it was" instead of a known owner: teardown that reads the current owner at session start and writes it back at session end restores the previous session's leftover value if that session also failed to restore. Record the intended original owner in configuration and restore to that - which is also what makes a startup reconciliation pass possible, since it needs a value to compare against.
- Treating
finallyas covering every way a session can end: it covers every way the process can end while still executing, which is not the same set. A killed process, an OOM kill, a stopped container and a power loss all skip it, and each leaves the resource owned by the last session indefinitely - the one outcome this weakness is about. Thefinallyis necessary and is not sufficient on its own.