Preventing PATH and Environment Variable Hijacking in Bash Scripts
AI generated
$_
#!/
Bash · Security · PATH Hijacking
Preventing PATH and Environment Variable Hijacking in Bash Scripts
from relative calls to hardened environments

PATH hijacking exploits the fact that Bash scripts resolve commands through an inherited, potentially manipulated PATH variable instead of using fixed, trusted paths. This article shows how a manipulated PATH or IFS tricks a script into running someone else's programs, why cron jobs and systemd units are especially affected, and which concrete measures harden Bash scripts against this attack pattern.

18 min read PATH Hijacking · IFS · Cron Jobs · command -p Bash 4.x · 5.x · Linux

1. What PATH hijacking is and how it compromises Bash scripts

PATH hijacking exploits the fact that Bash, given an unqualified command name like tar or python, searches the directories in the PATH environment variable in order and starts the first executable found with that name. If PATH contains a directory an attacker can write to, for example a directory in the user's home directory or the current working directory, the attacker can place a malicious program there with the name of a commonly used command. If that directory is searched before the actual system directory in PATH, the script unknowingly runs the malicious program instead of the expected system command.

The fundamental problem arises because PATH is an environment variable that a Bash script normally inherits unchanged from its parent process. A script starting in a compromised or carelessly configured environment, for example with the current working directory as the first PATH entry, is thereby structurally vulnerable to PATH hijacking, regardless of how carefully the actual script code is written.

PATH hijacking is particularly critical for scripts running with elevated privileges, such as as root in a cron job or in a CI runner with access to deployment credentials. A successful PATH hijacking attack against such a script runs foreign code with the same elevated privileges the script itself runs with, making this attack vector one of the most effective methods for privilege escalation in automation environments.

2. Relative command calls vs. absolute paths

The most effective single measure against PATH hijacking is calling security critical commands in Bash scripts by their absolute path instead of relying on PATH resolution. /usr/bin/tar instead of tar, /usr/bin/python3 instead of python3, and /bin/rm instead of rm ensure that the script always runs exactly the program the developer expected, regardless of which directories appear in PATH or in what order they are searched.

For scripts with many external command calls, it is worth defining the absolute paths centrally as read only variables at the start of the script, instead of scattering them throughout the code. This makes later audits easier and shows at a glance which external programs the script actually uses.


#!/usr/bin/env bash
set -euo pipefail

# Centralized absolute paths — resistant to PATH manipulation
readonly TAR_BIN="/usr/bin/tar"
readonly RSYNC_BIN="/usr/bin/rsync"
readonly OPENSSL_BIN="/usr/bin/openssl"

# UNSAFE: relies on PATH resolution
# tar -czf backup.tar.gz /data

# SAFE: absolute path, immune to a malicious "tar" earlier in PATH
"$TAR_BIN" -czf backup.tar.gz /data

"$RSYNC_BIN" -avz --delete /data/ backup-host:/backups/data/
"$OPENSSL_BIN" enc -aes-256-cbc -salt -in secret.txt -out secret.enc

A nice side effect of this pattern is reproducibility: a script with absolute paths behaves identically across different systems, as long as the referenced binaries live in the same locations, whereas a script with relative calls could run different program versions depending on the installed PATH configuration.

3. IFS manipulation as a related attack vector

Related to PATH hijacking but less well known is manipulation of the IFS variable, the Internal Field Separator, which determines which characters Bash splits words on during expansion. The default value of IFS is space, tab, and newline. If an attacker sets IFS to a different value before a script runs, for example to a slash, that can fundamentally change how paths and commands are interpreted, and in certain constellations cause a script to interpret unexpected words as separate command arguments or even as standalone commands.

A Bash script that does not explicitly set IFS to a safe default value at the start inherits the IFS value of the calling environment, just like with PATH. The hardening is structurally identical: set IFS explicitly at the start of the script instead of relying on the inherited environment, and never assume default values are guaranteed in every execution environment.


#!/usr/bin/env bash
set -euo pipefail

# Explicit, safe IFS at script start — never trust the inherited value
IFS=$'\n\t'

# Also reset PATH explicitly for the duration of this script
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

echo "PATH and IFS are now controlled, not inherited"

4. Hardening inherited environment variables in cron jobs and systemd units

Cron jobs are a particularly common location for PATH hijacking risks, because the default PATH variable in a cron environment is often minimal and differs significantly from the PATH variable of an interactive login shell. Many administrators react to this by setting a very broad PATH in the crontab that includes additional, potentially unsafe directories to avoid script errors. But this is exactly what opens the door to PATH hijacking, if one of those additional directories is writable by other users.

The safer solution is not to generously expand PATH in the crontab, but to equip every script with its own minimal, controlled PATH and exclusively absolute paths for critical commands. Systemd units offer even finer control with the Environment= and EnvironmentFile= directives, because they let you explicitly define which environment variables a service receives, without depending on an inherited, potentially manipulated shell environment.


# Crontab: avoid overly broad PATH, let the script set its own
# BAD:  PATH=/home/deploy/bin:/usr/local/bin:/usr/bin:/bin
# GOOD: minimal, predictable PATH, script handles the rest internally
PATH=/usr/bin:/bin
0 2 * * * /usr/local/bin/nightly-backup.sh >> /var/log/backup.log 2>&1

# /etc/systemd/system/nightly-backup.service
[Unit]
Description=Nightly backup job

[Service]
Type=oneshot
# Explicit, minimal environment — no inherited shell PATH
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ExecStart=/usr/local/bin/nightly-backup.sh

5. Setting a safe PATH explicitly at the start of a script

Regardless of the context in which a Bash script is ultimately executed, explicitly setting a minimal PATH containing only system directories at the start of the script is one of the most effective single measures against PATH hijacking. Such a PATH should only contain directories writable exclusively by root, typically /usr/local/sbin, /usr/local/bin, /usr/sbin, /usr/bin, /sbin, and /bin, and never the current working directory or a directory in an unprivileged user's home directory.

This pattern combines well with the absolute path approach shown in the previous section: even if a developer accidentally uses a relative command name, the explicitly set, minimal PATH ensures only trusted system directories are searched, significantly reducing the risk of a successful PATH hijacking attack, even though absolute paths remain the more robust first line of defense.

6. Function name collisions and command -p as a protection mechanism

Another often overlooked aspect of PATH hijacking concerns function name collisions: if a script or a sourced library defines a Bash function with the same name as a system command, for example a function named cd or ls, that function shadows the actual command for the rest of the script execution. This is not classic PATH hijacking, but follows the same basic pattern: the caller expects a specific program but actually gets something else executed.

The Bash builtin command -p elegantly solves this problem: it guarantees running a command with a safe, predefined PATH, ignoring both same named functions and the current PATH variable. In security critical scripts that define their own helper functions, command -p is therefore the most reliable way to ensure the expected system program actually runs.


#!/usr/bin/env bash
set -euo pipefail

# A library might accidentally define a function shadowing a builtin command
ls() {
  echo "custom logging wrapper"
}

# UNSAFE: this calls the shadowing function above, not /bin/ls
ls -la /var/log

# SAFE: command -p bypasses functions AND the current PATH entirely
command -p ls -la /var/log

7. Third party scripts and source: checking the chain of trust

Every source command in a Bash script pulls in foreign code with the same permissions the calling script runs with, including the ability to change PATH, IFS, and other security relevant environment variables. A third party script pulled in via source should therefore be reviewed just as carefully as your own code, especially if it comes from an external repository, a package manager, or a not fully trusted source.

A practical hardening pattern is to re verify PATH and other critical environment variables immediately after every source command and abort the script on any unexpected change. This prevents a compromised or carelessly written sourced library from silently undermining the main script's security assumptions.


#!/usr/bin/env bash
set -euo pipefail

readonly EXPECTED_PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
export PATH="$EXPECTED_PATH"

source ./lib/third-party-helpers.sh

# Verify PATH was not silently altered by the sourced library
if [[ "$PATH" != "$EXPECTED_PATH" ]]; then
  echo "[ERROR] PATH was modified after sourcing third-party code: $PATH" >&2
  exit 1
fi

8. Using audit tools and ShellCheck rules for PATH hardening

ShellCheck recognizes certain patterns that hint at PATH hijacking risks, such as relative calls to ./script.sh without checking whether the current directory is already dangerously present in PATH. An additional manual audit step should systematically search for unqualified command calls in security critical scripts and match every finding against the list of absolute paths shown in the previous section.

For recurring audits, a simple script that logs the current PATH variable in critical execution contexts such as cron jobs and systemd units, and checks it against an expected, minimal list of system directories, is useful. Every deviation, such as an additional directory writable by other users, should be reported as a finding before it leads to an actual PATH hijacking incident.


#!/usr/bin/env bash
set -euo pipefail

readonly SAFE_DIRS="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

check_path_safety() {
  local IFS=':'
  local -a dirs=($PATH)
  for dir in "${dirs[@]}"; do
    if [[ ":$SAFE_DIRS:" != *":$dir:"* ]]; then
      echo "[WARNING] Untrusted PATH entry: $dir" >&2
    fi
    if [[ -w "$dir" && "$dir" != "/usr/local/bin" ]]; then
      echo "[CRITICAL] World-writable-ish PATH entry: $dir" >&2
    fi
  done
}

check_path_safety

9. Comparison: unsafe vs. safe environment handling

The following table compares common unsafe patterns for handling PATH and environment variables against their safe counterparts.

Situation Unsafe Safe Benefit
Calling a system command tar -czf … /data /usr/bin/tar -czf … /data Immune to PATH manipulation
PATH in the script Adopting the inherited environment Explicitly set to system directories Controlled, predictable resolution
Cron job PATH Very broad PATH in crontab Minimal PATH, script sets the rest itself No additional writable directories
Command despite function name ls -la /var/log command -p ls -la /var/log Ignores functions and PATH alike
Including third party code source without re verification Re verify PATH after source Unnoticed manipulation is detected

Mironsoft

PATH audits and hardening of Bash automation

PATH hijacking risks in your cron jobs and deployment scripts?

We systematically audit PATH and IFS hardening in your Bash scripts, cron jobs, and systemd units, and replace risky relative calls with vetted, absolute paths.

PATH audit

Check every execution context for risky PATH values

Refactoring

Replace relative calls with absolute paths

Cron & systemd

Set up minimal, controlled environments for automation

10. Summary

PATH hijacking arises because Bash scripts resolve unqualified command names through an inherited, potentially manipulated PATH variable. Absolute paths for security critical commands are the most effective single countermeasure, because they bypass PATH resolution entirely. An explicitly set, minimal PATH at the start of a script, combined with a safe IFS value, closes the remaining attack surface for scripts that still use relative command names.

Cron jobs and systemd units deserve special attention, because their default environments are often carelessly configured with overly broad PATH values. command -p additionally protects against function name collisions, and re verifying PATH after every source command uncovers unnoticed manipulation by included code. Together, these measures form a robust defense against PATH and environment variable hijacking in Bash automation.

Preventing PATH Hijacking in Bash — The Key Points at a Glance

Core problem

Unqualified commands are resolved through an inherited, potentially manipulated PATH variable.

Most effective measure

Absolute paths for security critical commands, independent of the PATH value.

Explicit environment

Set PATH and IFS explicitly to minimal, safe values at the start of the script.

Cron & systemd

No overly broad PATH in the crontab, use Environment= in systemd units for controlled values.

11. FAQ: Preventing PATH Hijacking in Bash

1What is PATH hijacking?
Exploits how Bash resolves commands via PATH directories. Writable directories allow malicious programs with system command names.
2Why are absolute paths effective?
They completely bypass PATH resolution, the expected program always runs.
3IFS manipulation and PATH hijacking?
Both rely on unverified adoption of inherited environment variables.
4Why are cron jobs vulnerable?
Minimal default PATH is often compensated with an overly broad PATH in the crontab.
5PATH in systemd units?
Via Environment= with a minimal, explicit list instead of inherited shell environment.
6What does command -p do?
Runs commands with a safe, predefined PATH, ignoring functions and the current PATH.
7Why are function name collisions risky?
A same named function shadows the expected system command for the rest of execution.
8Why check source calls?
source includes code with full permissions, including the ability to change PATH.
9Does ShellCheck detect PATH risks?
Partially, but does not know the actual runtime PATH configuration. Manual audit remains necessary.
10Which directories belong in a safe PATH?
Only root-writable system directories, never the current working directory or user homes.