CWE-426: Untrusted Search Path
Overview
Untrusted Search Path occurs when a program locates an executable, library, or other critical resource using a search path supplied from outside the program - most often the inherited PATH, LD_LIBRARY_PATH, or LD_PRELOAD environment variables. Whoever sets that environment before the program starts decides which file it loads, so the program runs attacker-supplied code with its own privileges. It matters most for setuid binaries, services, scheduled tasks, and anything launched from a script whose environment a lower-privileged user can influence.
Relationship to Other CWEs
The line between this CWE and CWE-427 is who controls the path, not who controls a directory:
- CWE-426 (this page) - the search path itself is supplied from outside: an inherited
PATH,LD_LIBRARY_PATHorLD_PRELOAD, or a path read from configuration an attacker can edit. - CWE-427 (Uncontrolled Search Path Element) - the program controls the path, but one of the directories already on it is writable by someone who should not decide what loads.
MITRE notes the two are easily confused and that telling them apart 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 on the CWE-427 page. The hardening advice overlaps, so read both if the finding does not clearly fall on one side. CWE-114 (Process Control) covers untrusted library loading and process execution more broadly, with language-specific pages for C, C#, Java, JavaScript and Python.
OWASP Classification
A08:2025 - Software or Data Integrity Failures
Risk
High: The attacker points the search at a directory they control, so the file they placed there runs with the program's privileges - on a setuid binary or a service, that is privilege escalation.
Remediation Steps
Core Principle: Resolve executables and libraries to absolute paths the program controls, so no externally-supplied search path is ever consulted.
Locate the untrusted search path vulnerability
- Identify what is being loaded: an executable, a library or DLL, a shared object, or a configuration file
- Identify how it is located: a relative path,
PATH,LD_LIBRARY_PATH, or the platform's default search order - Work out who can set that search path before the program starts, and whether any directory it consults is attacker-controlled
Specify full paths for executables and libraries (Primary Defense)
- Name the resource by absolute path -
/usr/local/bin/mytool, notmytool- soPATHis never consulted. Hard-code the trusted location:/usr/bin/python3,/opt/app/bin/script,C:\Program Files\App\tool.exe - Verify the file exists at that absolute path before running it
- Resolve relative paths against a fixed base rather than the working directory - the install directory or a configured root. The working directory is attacker-influenced, so joining a relative name to it produces an absolute path that is no more trustworthy than the name was
Restrict writable directories in search path
- Give directories on
PATHrestrictive permissions, 755 or 700, so only trusted users can write to them - Remove
.,./and world-writable directories fromPATH - Keep user home directories such as
~/.local/binoff the path a system service searches
Use safe loading functions and mechanisms
- Prefer APIs that do not consult the inherited path. Pass
dlopen()an absolute path so no search happens at all; on Windows callSetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_SYSTEM32)at startup, or useLoadLibraryExwith the matchingLOAD_LIBRARY_SEARCH_*flag.RTLD_NOLOADis not a search restriction - it only reports whether an object is already resident - Do not launch bare names through a shell.
system()andpopen()hand the name to the platform shell, which applies its own resolution rules on top ofPATH - Load libraries from specific paths rather than leaving them to the system search
- Account for the current directory where the platform still searches it. It is not on a POSIX
PATHunless something put it there, butcmd.exeresolves a bare command name from the current directory before consultingPATH; settingNoDefaultCurrentDirectoryInExePath=1removes it
Monitor and audit resource loading
- Log resource loading events: executable launches, library loads, file accesses
- Alert on loads from temp directories and other user-writable locations
- Track changes to the search path configuration
- Watch for DLL hijacking attempts on Windows
- Audit filesystem permissions on the directories the search consults
Test the search path fix
- Start the program with
PATHpointing at a directory holding a trojan of the same name: the real binary runs, and the trojan leaves no trace - Start it with
PATHandLD_LIBRARY_PATHempty: the legitimate call still succeeds, which proves the search was removed rather than merely redirected - Place a trojan in the working directory the program is started from: it is not executed
- Confirm normal invocation still works after the change - a fix that resolves to the wrong absolute path fails every one of the tests above in exactly the same way as a correct one
- Test on Windows, Linux and macOS, since what the current directory contributes differs between them
- Re-scan to confirm the finding is resolved
Common Vulnerable Patterns
- Loading executables or libraries by name only, leaving resolution to the inherited environment
- Passing the caller's environment straight through to a child process
Relative Executable Path Resolution (Pseudocode)
// VULNERABLE - Relative Executable Path Resolution
// The name is resolved against whatever search path the process inherited
run_process('mytool')
// Attack: start the program with PATH=/tmp/evil:$PATH
// Result: /tmp/evil/mytool runs with the program's privileges
Why this is vulnerable: The program takes its search path from the environment it was started with, so whoever sets that environment chooses which mytool runs. The attacker does not need a writable directory that is already on the path - adding one is the attack, which is what separates this from CWE-427. Routing the call through a shell widens it further: cmd.exe resolves a bare command name from the current directory before it consults PATH at all, so on Windows a working directory the attacker can write to is enough on its own.
Secure Patterns
Absolute Path for Executable (Pseudocode)
// SECURE - the path is fixed, so no search takes place
run_process('/usr/local/bin/mytool')
// Where a child must still resolve names, give it a known environment
// rather than passing on the one this process inherited.
run_process('/usr/local/bin/mytool', env = { PATH: '/usr/local/bin:/usr/bin' })
Why this works:
- An absolute path means no search happens, so the contents of
PATH,LD_LIBRARY_PATHand the current directory stop being security-relevant for this call - Building the child's environment explicitly stops it inheriting a search path the attacker chose for the parent, which is the same weakness one process further down
- It does not harden the child. Once
/usr/local/bin/mytoolis running, its own library search order is a separate question - the loader flags that answer it are on the CWE-114 language pages, and the writable-directory half is CWE-427 - On a set-user-ID binary the loader drops
LD_LIBRARY_PATHentirely, so a finding there is usually about a path the program builds itself rather than one it inherits.LD_PRELOADis narrowed rather than ignored: preload pathnames containing slashes are dropped, and a shared object is preloaded only from the standard search directories and only if it carries the set-user-ID mode bit. That combination is uncommon, but "setuid means no preloading" is not what the loader does