CWE-402: Transmission of Private Resources into a New Sphere ('Resource Leak')
Overview
Transmission of a private resource into a new sphere happens when a resource handle a program holds - an open file descriptor, a listening or connected socket, a database cursor - survives into a context that was never meant to hold it. The usual route is a child process inheriting whatever the parent had open across exec, but the same shape appears when a handle outlives a privilege drop or is left open for the next user of a shared connection. Nothing is copied and no data is deliberately sent: what crosses the boundary is the capability to use the resource, still carrying the access rights it was opened with.
Relationship to Other CWEs
- CWE-402 (this page) - a resource handle that crosses into a control sphere it should not reach
- CWE-668 (Exposure of Resource to Wrong Sphere) - the parent
- CWE-403 (Exposure of File Descriptor to Unintended Control Sphere) - this page's child covering descriptor inheritance, which is what both of MITRE's observed examples for CWE-402 describe. No page here, so this page stands in for it
- CWE-619 (Dangling Database Cursor) - the other child: a cursor left open and reachable with the privileges it was created under. No page here either
A private file served to a client over HTTP is better mapped elsewhere, even though the words look similar. CWE-402 is about the handle, not the bytes. Route those findings to:
- CWE-22 - a caller-supplied path escaping the intended directory
- CWE-548 - a directory listing revealing what exists
- CWE-538 - a backup, VCS or config file reachable in the webroot
- CWE-200 - a response carrying fields the caller should not see
OWASP Classification
A01:2025 - Broken Access Control
Risk
High: an inherited descriptor is a live grant of whatever access it was opened with, and it survives the checks that guarded the original open. A child process spawned to run untrusted or lower-privilege work can read a private key file the parent opened as root, or write to a log or database the sandbox was supposed to deny it. Where the leaked handle is a listening socket or a control channel, the child can take over the service's endpoint. A dangling database cursor gives the next holder of the session the privileges the cursor was created with, which is the usual route from cursor leak to SQL injection.
Remediation Steps
Core Principle: A handle must not survive a boundary crossing it was not explicitly granted - open it close-on-exec, and pass a child or a successor only the specific handles it needs.
Trace the Handle
- Source: Where the handle is acquired, and with what rights - a file opened before a privilege drop, a socket bound to a privileged port, a cursor opened under a definer's rights, a connection taken from a pool.
- Sink: The boundary the handle crosses -
execof a child process, aforkfollowed by running someone else's code, a privilege drop, returning a connection to a pool, or the end of a request that leaves the cursor open. - Missing control: The handle was not marked close-on-exec (or the runtime's inherit-everything default was left in place), the spawn passed the whole descriptor table rather than a chosen set, or an error path skipped the close.
Make Handles Close-on-Exec at Open Time (Primary Defense)
Request close-on-exec as part of the call that creates the handle, not as a second step:
- Pass
O_CLOEXECtoopen(and the equivalent flag on socket, pipe and accept calls). Theopen(2)man page gives the reason the flag exists: setting close-on-exec afterwards withfcntlraces afork/execvein another thread, and the descriptor can be inherited before the second call lands. - Where an API cannot take the flag, set
FD_CLOEXECwithfcntl(fd, F_SETFD, ...)immediately after the open, and treat the window as a known residual risk in multithreaded code. posix_spawnfile actions let the caller close or remap specific descriptors as part of the spawn, but anything not named is still inherited under ordinaryexecsemantics.posix_spawn(3)is explicit that with no file actions "file descriptors open before the exec remain open in the new process, except those for which theFD_CLOEXECflag has been set". Open-timeO_CLOEXECremains the control; enumerating closes at spawn time is not an alternative to it.
Managed runtimes mostly default to non-inheritance already, and the finding is usually an explicit opt-in that was added for convenience:
- Python has created descriptors non-inheritable by default since 3.4 (PEP 446). Descriptors from
os.open,open, andsocketget close-on-exec on Unix and a clearedHANDLE_FLAG_INHERITon Windows, andos.set_inheritable()is the deliberate opt-back-in. The default covers descriptors Python itself creates; one opened inside a third-party C extension is not affected.subprocessalso closes non-standard descriptors in the child unlesspass_fdsnames them. - Java
ProcessBuildergives the child only the streams it redirects; by default those are pipes back to the parent.inheritIO()is the opt-in that hands the child the JVM's own standard streams. - Node.js
child_processspawns with piped stdio by default.stdio: 'inherit'passes the parent's streams through, and a positive integer in thestdioarray shares that parent descriptor with the child - both are explicit grants, so check that each one is needed. - Go
exec.Cmdpasses nothing beyond the configured standard streams unlessExtraFilesis set; entries there become descriptors 3, 4, ... in the child. AnExtraFilesentry is the grant to justify. The field is documented as unsupported on Windows.
Pass Only the Handles the Far Side Needs
Where a child needs a resource, hand it one specific descriptor rather than the parent's table, and hand it the narrowest one available: a read-only descriptor rather than read-write, a descriptor for the single file rather than one for the directory. If the child only needs data, prefer copying the data over a pipe to passing the handle at all. A pipe carries bytes the parent chooses and the child sees EOF once every write end is closed, whereas a passed descriptor carries the access rights it was opened with and lets the child read whatever the resource holds.
Close Handles Before Crossing a Trust Boundary
A privilege drop does not revoke handles already open. Anything opened while privileged - a key file, a bound port, an audit log - keeps its original access after the process drops to the service account, so close what the post-drop code does not need before dropping, and re-open the rest as the lower identity. The same applies to a connection or worker handed on to another tenant's request.
Close Cursors, Statements and Connections on Every Path
Close database cursors, prepared statements and result sets in the construct that runs on the error path too, not only on success - an unhandled exception is the usual way a cursor is left dangling. Return connections to the pool in a reset state so no cursor, temporary table or session variable carries into the next borrower.
Test the Fix
- Spawn the child the finding names and enumerate the descriptors it actually holds (
/proc/<pid>/fdon Linux,lsof -p), and confirm only the intended ones are present. - Make the child attempt the access the leaked handle would have granted - reading the key file, writing the privileged log - and confirm it fails.
- Force an exception in the middle of a cursor's lifetime and confirm the cursor is closed anyway.
- Re-scan with the security scanner to confirm the finding is resolved.
Common Vulnerable Patterns
The usual shape is a handle opened under one set of rights and still open when the process crosses into a context with fewer: the spawn never mentions the descriptor, so nothing in the code reads as a grant of access.
// VULNERABLE - pseudo-code
key_fd = open('/etc/service/private.key') // opened while privileged
drop_privileges_to('service')
spawn('/usr/bin/render', untrusted_input) // child inherits key_fd
// Attack: the child process reads the private key through the inherited
// descriptor, having never been granted access to the path
// Result: a key the OS would have denied the child is readable by it
Secure Patterns
// SECURE - pseudo-code
key_fd = open('/etc/service/private.key', CLOSE_ON_EXEC)
drop_privileges_to('service')
spawn('/usr/bin/render', untrusted_input,
handles = [stdin_pipe, stdout_pipe, stderr_pipe]) // nothing else
Why this works: the close-on-exec flag is set atomically by the call that creates the descriptor, so there is no window in which another thread's exec can inherit it, and the kernel closes it during exec without the parent having to remember to. Naming the child's handles explicitly makes inheritance a grant rather than a default, so a descriptor opened later somewhere else in the process does not silently join the set the child receives.
Common Pitfalls
- Setting close-on-exec after the open: correct in a single-threaded program, racy in a threaded one. Another thread can
forkandexecbetween theopenand thefcntl, and the child gets the descriptor. Use the open-time flag wherever the API offers one. - Fixing the descriptor the finding names and stopping there: the reported line is a sample. Every handle open at the moment of the spawn is inherited under the same default, so audit what else is open on that path rather than flagging one descriptor.
- Treating a privilege drop as revocation: dropping to an unprivileged account stops the process opening new privileged resources; it does nothing to the ones already open. The handles have to be closed separately.
- Closing the language-level object and assuming the descriptor is gone: a stream, reader or connection wrapper that has been garbage-collected but not closed may still hold the underlying descriptor until finalization, which can be after the spawn. Close it explicitly.
Additional Resources
- CWE-402: Transmission of Private Resources into a New Sphere ('Resource Leak')
- CWE-403: Exposure of File Descriptor to Unintended Control Sphere - the descriptor-inheritance child
- CWE-619: Dangling Database Cursor ('Cursor Injection') - the database-cursor child
- open(2) man page -
O_CLOEXECand why it is preferred over setting the flag afterwards - fcntl(2) man page -
FD_CLOEXEC, for handles created by an API that cannot take the flag - posix_spawn(3) man page - what a spawn inherits when no file actions are given, and what file actions do not change
- PEP 446: Make newly created file descriptors non-inheritable
- ProcessBuilder (Java SE 17 API documentation) - the default pipe redirection, and what
inheritIO()hands the child - Node.js child_process -
stdio: 'inherit'and the numeric file descriptor entries that share a parent descriptor - Go os/exec: Cmd -
ExtraFilesentries becoming descriptors 3+i, and the Windows limitation - OWASP Top 10 2025 A01: Broken Access Control