Skip to content

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_PATH or LD_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, not mytool - so PATH is 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 PATH restrictive permissions, 755 or 700, so only trusted users can write to them
  • Remove ., ./ and world-writable directories from PATH
  • Keep user home directories such as ~/.local/bin off 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 call SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_SYSTEM32) at startup, or use LoadLibraryEx with the matching LOAD_LIBRARY_SEARCH_* flag. RTLD_NOLOAD is not a search restriction - it only reports whether an object is already resident
  • Do not launch bare names through a shell. system() and popen() hand the name to the platform shell, which applies its own resolution rules on top of PATH
  • 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 PATH unless something put it there, but cmd.exe resolves a bare command name from the current directory before consulting PATH; setting NoDefaultCurrentDirectoryInExePath=1 removes 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 PATH pointing at a directory holding a trojan of the same name: the real binary runs, and the trojan leaves no trace
  • Start it with PATH and LD_LIBRARY_PATH empty: 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_PATH and 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/mytool is 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_PATH entirely, so a finding there is usually about a path the program builds itself rather than one it inherits. LD_PRELOAD is 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

Additional Resources