Skip to content

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_PATH or LD_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_PATH or PYTHONPATH unless there is no alternative
  • Set PATH to a known-good value such as PATH = '/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.loadLibrary keeps 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 in main or in a static block, that gets ahead of it (measured on JDK 26). Set it with -Djava.library.path at launch, or load by absolute path with System.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 /opt does not let an attacker touch /opt/app/bin - it lets them rename /opt/app, and supply their own app/bin underneath, 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 carrying S_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 /tmp cannot be renamed out from under you. It says nothing about the directory carrying the bit - /tmp itself resists replacement because / is root-owned, not because of its own mode
  • On Windows, read the same question through icacls rather than a mode. The drive root grants Authenticated Users the right to add subdirectories and, by inheritance, to modify what they contain, so a top-level directory such as C:\tools or a language runtime installed to C:\ is writable by any logged-in user and is frequently on the system PATH. Check every element with icacls <dir> and look for Authenticated Users, Users or Everyone carrying (W), (M), (F) or (AD)
  • Remove ., ./, /tmp, /var/tmp and 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_PATH and 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/bin can be unrenameable while /opt/app is not, and whoever replaces /opt/app has 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 PATH at all - it uses RPATH, LD_LIBRARY_PATH, RUNPATH, the ld.so cache and the default directories - so tightening PATH does nothing for library loading there. On Windows PATH is 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