CWE-427: Uncontrolled Search Path Element
Overview
Uncontrolled Search Path Element occurs when a program uses a search path it controls, but one of the locations on that path can be written to by someone who should not decide what the program loads - the current directory, /tmp, a per-user install directory, or a PATH entry with loose permissions. The attacker never touches the path itself; they drop a file into a directory that is already on it, and the loader finds it in the ordinary way.
Relationship to Other CWEs
- CWE-427 (this page) - the search path is fixed or controlled by the program, and one of its elements is writable by an untrusted principal.
- CWE-426 (Untrusted Search Path) - the search path itself comes from outside: an inherited
PATH,LD_LIBRARY_PATHorLD_PRELOAD. - CWE-668 (Exposure of Resource to Wrong Sphere) - the parent, in both Research Concepts and the simplified mapping view. It is where this weakness sits in the tree, not somewhere to file a finding: MITRE marks CWE-668 Discouraged for mapping, precisely because it gets used as a catch-all when a lower-level child would have fitted.
MITRE notes that this entry and CWE-426 are easily confused and that separating them takes root-cause analysis. The practical test is whether the attacker had to change the path: if they only had to write a file into a directory already on it, the finding belongs here. Three of the names in circulation - DLL preloading, binary planting, insecure library loading - are used for both, so do not take the label in a scanner finding as the answer. Dependency confusion is the exception: MITRE attributes it to this entry specifically, for the package-dependency case. CWE-114 (Process Control) covers untrusted library loading and process execution more broadly, with language-specific pages for C, C#, Java, JavaScript and Python - read it for the loader detail, but MITRE marks it Discouraged for mapping, so it is not where this finding should be filed. This entry's own mapping usage is Allowed-with-Review, because of the confusion with CWE-426 this paragraph opens with - settle which of the two applies before filing.
OWASP Classification
A08:2025 - Software or Data Integrity Failures
Risk
High: Any user who can write to one of the directories the program already searches can plant an executable or library there and have it loaded with the program's privileges. Where the program is more privileged than the user who planted the file, that is a privilege escalation.
Remediation Steps
Core Principle: Every location the program searches must be writable only by principals already trusted to supply its code, and the fewer locations it searches, the better.
Locate the uncontrolled search path element
- Review the flaw details to identify which search the finding is about: executable lookup on
PATH, native library loading, module or package resolution, or plugin discovery - Enumerate every location that search consults, including the ones nobody wrote down: the application's own directory, the current working directory, and whatever defaults the loader appends
- Check the permissions on each one from an ordinary user account. A per-user install under a user's profile,
/tmp,/var/tmp, and any directory a non-administrator can create files in are the elements that matter - Look for empty elements in
PATH. A leading:, a trailing:, or::each mean the current working directory on POSIX systems, and are easy to produce by joining a variable that turned out to be unset - For package resolution, check whether a public registry is consulted for names that are meant to come from a private one
Set search paths explicitly (Primary Defense)
- Name critical resources by absolute path, so no search happens at all
- Do not append to
PATH,LD_LIBRARY_PATHorPYTHONPATHunless there is no alternative - Set
PATHto a known-good value such asPATH = '/usr/local/bin:/usr/bin:/bin'. Overwrite it rather than append, because appending inherits whatever elements were already there - Where a path must be changed, change it for the subprocess rather than for the whole process
- Build a clean environment for a subprocess instead of inheriting the one you were started with
- Do not expect a runtime property to move a loader's path.
System.setProperty("java.library.path", ...)after the JVM has started changes the property and nothing else;System.loadLibrarykeeps using the value captured at JVM startup, before any of your code runs - so there is no ordering trick, such as setting it first thing inmainor in a static block, that gets ahead of it (measured on JDK 26). Set it with-Djava.library.pathat launch, or load by absolute path withSystem.load
Restrict writable directories in search path
- Confirm only trusted users can write to the directories on the path: permissions of 755 or more restrictive
- Check the ancestors, not just the element. Renaming a directory needs write permission on its parent, so a group-writable
/optdoes not let an attacker touch/opt/app/bin- it lets them rename/opt/app, and supply their ownapp/binunderneath, which replaces the path element just as effectively while every mode on the element itself stays 755. Walk up to the filesystem root and apply the same rule at every level. The sticky bit narrows this one level down and no further: on a directory carryingS_ISVTX, an entry inside it may be renamed only by that entry's owner, the directory's owner, or a privileged process, so a directory of yours sitting in world-writable/tmpcannot be renamed out from under you. It says nothing about the directory carrying the bit -/tmpitself resists replacement because/is root-owned, not because of its own mode - On Windows, read the same question through
icaclsrather than a mode. The drive root grantsAuthenticated Usersthe right to add subdirectories and, by inheritance, to modify what they contain, so a top-level directory such asC:\toolsor a language runtime installed toC:\is writable by any logged-in user and is frequently on the systemPATH. Check every element withicacls <dir>and look forAuthenticated Users,UsersorEveryonecarrying(W),(M),(F)or(AD) - Remove
.,./,/tmp,/var/tmpand user home directories from the search path - Remove empty elements. A leading, trailing or doubled separator is the current working directory on POSIX systems, and reads as a formatting slip rather than as a security defect
- Order is not a substitute for removal. Putting system directories ahead of user ones decides the winner only for a name that exists in both. A name that appears in none of the trusted directories is still resolved from whatever untrusted element remains, however far down it sits, and planting a name nothing legitimate provides is the easier attack. Order the path by all means, but a writable element is not fixed until it has been removed from the path or its permissions have been tightened
Check each element before you add it
Where the program legitimately extends its own search path - a plugin directory, a bundled runtime, a private package feed - the element it adds is the one that has to be justified.
- Before a directory joins the path, confirm it is owned by root, Administrators or the service account, and is not writable by anyone else
- Canonicalize first, resolving symlinks and
.., so the directory you inspected is the directory that will be searched - Run the same ownership check over every directory above it, up to the filesystem root. Canonicalization tells you which directory you are about to search; it does not tell you who is able to replace it between the check and the load
- If an element fails the check, fail startup. Searching it anyway and logging a warning leaves the weakness in place
- Never build an element from a value that can be empty. Joining an unset variable produces an empty element, which POSIX reads as the current working directory
- Apply the same rule to package sources. A public registry left in a private project's resolution order is the dependency-confusion form of this weakness. Scope internal names to the private feed, reserve those names publicly where the registry allows it, and pin with a lock file carrying hashes so a substitution is a build failure rather than a silent swap
Monitor and audit path usage
- Log changes to search paths, including
PATH,LD_LIBRARY_PATHand other environment variable modifications - Log resource loading, so that a search path being exploited leaves a trace
- Alert on unexpected path modifications, such as the addition of an unusual directory
- Track which executables are run from which directories
- Watch for privilege escalation via path manipulation
Test the search path control fix
- Place a trojan named after each searched resource in every directory still on the path, one at a time, and record which copy wins. This confirms the search order you believe you have, not that the path is safe: an element ahead of the real resource shadows it, exactly as the ordering bullet above says it will. The assertions that model the attacker are the unprivileged write and rename tests below
- Run the application normally afterwards: it still starts and still finds everything it needs. This is the assertion that catches over-trimming, and nothing below it will: a path with one element too few passes every rejection test here exactly as a correct one does, because a directory that is no longer on the path cannot be written to or renamed into place either
- Split the path the application ends up with on the separator and assert no entry is zero-length and none is
. - From an unprivileged account, attempt to create a file in each remaining element: every attempt is denied
- From the same account, attempt to rename each remaining element and every directory above it -
/opt/app/bin, then/opt/app, then/opt, up to the root - putting a directory of your own in each one's place: every attempt is denied. Testing the element alone is not enough, because/opt/app/bincan be unrenameable while/opt/appis not, and whoever replaces/opt/apphas replaced the element underneath it. This is the test that fails where the file-creation test passes - Drop a file in the working directory the service is started from: it is not loaded
- Test on different operating systems, since which directories the loader adds for you differs between them
- Re-scan with the security scanner to confirm the finding is resolved
Common Vulnerable Patterns
- Including a world-writable or per-user directory in an otherwise fixed search path
- Using relative paths for critical resources
Writable Element on a Fixed Search Path (Pseudocode)
// VULNERABLE - Writable Element on a Fixed Search Path
// The program sets its own path - the attacker never touches it
env.PATH = '/opt/app/bin:/tmp:/usr/bin'
run_process('helper')
// Attack: any local user creates an executable /tmp/helper
// Result: it runs with the application's privileges
Why this is vulnerable: The path here is not attacker-supplied - the program hard-codes it, which is exactly what the fix for CWE-426 asks for. It is still exploitable, because /tmp is on it and /tmp is world-writable, so the first user to create /tmp/helper chooses what the application executes. The same shape appears without a literal /tmp: an application directory under a user's profile in a per-user install, a PATH entry left group-writable by an installer, or an empty element produced by joining a variable that was unset, which POSIX reads as the current working directory.
Secure Patterns
Fixed Trusted PATH (Pseudocode)
// SECURE - no search happens, so no element can answer
run_process('/usr/bin/python3', 'script.py')
// Where a search is unavoidable, every element is root-owned:
// written out in full, so there is no '.', no /tmp and no empty entry
env.PATH = '/usr/local/bin:/usr/bin:/bin'
Why this works:
- The absolute path removes the search, so no element of any path can answer for
python3 - Where a path is still needed, every directory on it is writable only by root, leaving nowhere for an attacker to drop a file the loader would find
- Writing the list out in full rather than appending to what was inherited is what removes
.and empty elements - appending preserves them - This constrains the executable lookup only, and native library loading is a separate search with a different list on each platform. On POSIX the dynamic linker never consults
PATHat all - it usesRPATH,LD_LIBRARY_PATH,RUNPATH, theld.socache and the default directories - so tighteningPATHdoes nothing for library loading there. On WindowsPATHis consulted, but last, after the application's own directory, so it does not by itself prevent DLL hijacking either. The loader controls that do work are on the CWE-114 language pages
Additional Resources
- CWE-427: Uncontrolled Search Path Element
- Microsoft: Dynamic-link library search order - where each searched directory sits, and what
SetDefaultDllDirectoriesand theLOAD_LIBRARY_SEARCH_*flags remove - OWASP Secure Coding Practices Quick Reference Guide
- OWASP Top 10 2025 A08: Software or Data Integrity Failures