CoursesAdvanced scripting for DevSecOpsBash for secure, resilient automation

Hardening Bash: injection, IFS, PATH & temp races

Command/argument injection, IFS attacks, PATH hygiene, safe temp files and privilege drop.

Expert40 min · lesson 5 of 15

Bash sits at the exact spot attackers love: it stitches together untrusted input, external commands, files, and environment. Hardening a script means assuming every input is hostile — arguments, environment variables, filenames, the contents of files you read — and removing the ways that input can become a command, hijack a path, or win a race. These are the bugs that turn a helper script into a local privilege escalation.

Command and argument injection

The root cause is letting data cross into the code position of a command. eval on any string built from input is the worst offender; so is an unquoted variable that undergoes word splitting and globbing, and so is passing user text where a flag is expected (argument injection — a value starting with - becomes an option). Quote every expansion, end option parsing with --, and never eval untrusted data.

injection: the bugs and the fixes
# WORD SPLITTING/GLOB: filename with spaces or * wrecks this
rm -rf $dir/* # BAD
rm -rf -- "$dir"/* # quote; -- ends options
# ARGUMENT INJECTION: $user = "--checkpoint-action=exec=sh" etc.
grep "$pattern" file # if $pattern begins with -, it is read as a flag
grep -e "$pattern" -- file # -e forces it to be the pattern; -- ends options
# EVAL: never build code from data
eval "cp $src $dst" # BAD — $src='a; rm -rf /' is game over
cp -- "$src" "$dst" # just run the command with quoted args

IFS, PATH, and environment hygiene

Two environment variables silently change how your script behaves. IFS controls word splitting; if an attacker can set it (or you leave it at the default when reading data), fields split in surprising places. PATH controls which binary "curl" or "python" actually runs; a writable or attacker-controlled directory early in PATH means you execute their program. For anything privileged, set a known IFS, set an absolute PATH, and consider calling critical tools by full path.

lock down the execution environment
#!/usr/bin/env bash
set -Eeuo pipefail
IFS=$'\n\t' # predictable splitting
export PATH='/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
export LC_ALL=C # stable sort/format behaviour
# for the most sensitive calls, do not trust PATH at all
/usr/bin/install -m 0600 -- "$src" "$dst"

Temp-file races and safe privilege drop

A predictable temp path like /tmp/mytool.$$ is a symlink attack: an attacker pre-creates it pointing at a file you will then write as root. mktemp creates a file or directory with a random name and safe permissions atomically — always use it. And if your script starts as root only to do one privileged step, drop back to an unprivileged user for the rest so a later bug is not running with full power.

safe temp files, atomic writes, privilege drop
# atomic, unpredictable, and cleaned up
tmp="$(mktemp -d /tmp/tool.XXXXXX)"
trap 'rm -rf "$tmp"' EXIT
# write-then-rename = readers never see a half-written file
printf '%s' "$payload" > "$tmp/out"
mv -f "$tmp/out" /etc/app/config # rename is atomic on one filesystem
# do the root step, then drop privileges for everything after
chown root:root /etc/app/config
exec setpriv --reuid=app --regid=app --clear-groups "$0" --worker "$@"
The Bash attack surface, and the control for each
input becomes code
eval / unquoted $var
quote everything; never eval data
value read as a flag
end options with --
command substitution of input
validate before use
environment hijack
PATH
set absolute; call critical bins by path
IFS
set to newline+tab explicitly
LD_PRELOAD / locale
sanitize env for privileged runs
filesystem races
predictable temp names
mktemp only
half-written files
write-then-rename
symlink following
validate targets; use -- and O_NOFOLLOW tools
Assume every argument, env var, and filename is chosen by an attacker; each cell is one CVE class in shell scripts.
ShellCheck is not optional for security
Nearly every injection and word-splitting bug above is one ShellCheck warning (SC2086 unquoted, SC2046 word split, SC2091 eval, and friends). Run it on every script and treat findings as errors, not suggestions. A human will forget one quote in a 200-line script; the linter never does — wire it into CI so an unquoted expansion cannot merge.