In a Unix system, executable files may be located in many different places. Your path environment variable tells the shell where to look when you execute a particular command. It does this by storing a colon-separated string of paths, the order of which determines which executable is executed if there are multiple files with the same name. Whenever you execute a command, the shell searches through each path, in order, until it finds the first executable name matching your command.

At any point, you can check the value of your path by echoing it from your shell:

$ echo $PATH
/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin

Let's say you wanted to run a command like ls; the shell would search your path, find the matching executable at /bin/ls, and run it.

The trojan horse

I recently gave the following advice on Twitter:

Let's explore why this is bad.

Most people (and scripts) don't use fully qualified paths when executing commands. As a result, the shell will always try to find the first match from your path. Assume your path has been modified to prepend $HOME/.local/bin as in the bad example above. Your path should now look something like this:

$ echo $PATH
$HOME/.local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin

Now, assume there is a second copy of ls which lives in $HOME/.local/bin. The next time you try to list your current directory, it's going to run the copy in $HOME/.local/bin and not the system's executable in /bin. In a best-case scenario, you have multiple copies of the same binary, and the impact is minimal. In a worst-case scenario, the doppelganger file runs something entirely unexpected, or even malicious. This can escalate quickly.

What can we do?

The easiest advice is to ensure your system's paths always come first. You can override your path variable at any time by setting and exporting it. As a test, you can do this manually in your shell, and if everything looks good you can store this in your shell's rc file for persistence. (For example .bashrc for bash users, or .zshrc for zsh users.)

Realistically, your path really should start with the following:

/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin

Then, any additional paths can be appended as needed. We can manually re-oder the bad example above like this:

$ export PATH=/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:$HOME/.local/bin

And our shell will execute the proper system binaries when needed.

What if we want to supercede system tools?

In general, it's bad practice to do this. You really have to understand the impacts of upgrading core pieces of your OS before you try to do this. Superceding system binaries can have unintended side effects if other parts of the system are relying on specific versions of functionality. But if you really need to, there are two ways to go about it:

  • (Riskier) Overwrite the system binary with a newer version (not recommended.)

  • (Less risky) Place a second copy in a path like /opt/bin, and use the fully qualified path to call that version. (As @PineberryFox put it, "full paths are your friend.")

But, is this actually dangerous?

You may be tempted to believe the only way an attacker could abuse this would be to have access to your profile or filesystem, but there are many ways this could be exploited.

A very common thing I see is tooling with package managers prepending paths to your system's path. Things like homebrew, npm, python, rust, etc. Package managers are constantly the targets of hundreds and thousands of continuous supply chain attacks. As some examples, this article on npm, or this articke on pypi, and countless others. All it takes is one bad dependency, or even one bad dependency of a dependency of a dependency, to put malicious code on your system.

At the end of the day, when the fix is as trivial as exporting a new version of your path, why not give yourself this extra layer of protection?