Bash Script Hardening Checklist
A Bash script hardening checklist is a fixed set of checks applied to a script before it runs. Each check maps to one safety rule, and a script ships only when every check passes.
An audit counts flaws in a sample script
A sample script holds one dangerous line, one unquoted remove, and one safely quoted copy. The audit counts each pattern separately.
printf 'eval "$cmd"\nrm $file\ncp "$src" "$dst"\n' > target.sh
echo "eval count: $(grep -c -F eval target.sh)"
echo "unquoted rm count: $(grep -c -F 'rm $' target.sh)"
echo "quoted copy count: $(grep -c -F 'cp "$src"' target.sh)"A clean script counts zero flaws
The fixed script quotes its copy and guards its remove with --. Both danger counts fall to zero.
printf 'cp "$src" "$dst"\nrm -- "$file"\n' > fixed.sh
echo "eval count: $(grep -c -F eval fixed.sh)"
echo "unquoted rm count: $(grep -c -F 'rm $' fixed.sh)"Safe settings handle crafted names at once
Noclobber, pipefail, and a strict mask are all switched on. The quoted loop checks a spaced name and a dash-led name, and the spaced file lands private.
set -o noclobber
set -o pipefail
umask 077
printf 'x\n' > "a b.txt"
printf 'x\n' > ./-n.txt
count=0
for f in *.txt; do
count=$((count+1))
printf 'checked: %s\n' "$f"
done
echo "count: $count"
stat -c %a "a b.txt"Example
printf 'cp "$src" "$dst"\nrm -- "$file"\n' > sample.sh
fail=0
if grep -q -F eval sample.sh; then
echo "eval: FAIL"
fail=1
else
echo "eval: PASS"
fi
if grep -q -F 'rm $' sample.sh; then
echo "unquoted rm: FAIL"
fail=1
else
echo "unquoted rm: PASS"
fi
if grep -q -F '"$src"' sample.sh; then
echo "quoting: PASS"
else
echo "quoting: FAIL"
fail=1
fi
umask 077
printf 'x\n' > audit.txt
echo "umask mode: $(stat -c %a audit.txt)"
echo "failures: $fail"Key points
- Quote check: every variable expansion that holds a filename or value is quoted.
- Option check: commands that take filenames use -- or a ./ prefix for dash-led names.
- Eval check: the script contains no eval on untrusted input, with a safe alternative instead.
- Environment check: PATH is fixed and untrusted variables are dropped before outside commands run.
- Overwrite check: noclobber is on, and every deliberate overwrite uses >| on purpose.
- Pipeline check: pipefail is on, so a failed early pipeline stage cannot hide.
- Mode check: umask is set before secret files are created, and modes are verified with stat.
In this example, a sample script is checked rule by rule. The eval check and the unquoted-rm check both pass because neither pattern appears. The quoting check passes because the quoted source is present, and the secret audit file is created under the strict mask with no failures recorded.
Try it Yourself
Edit the code, then press Run.
Running is simulated
Exercise
An audit finds eval on user input in a script. What is the hardening response?