Shell scripts often run with elevated privileges, process untrusted input, and manage system resources. A poorly written script can be exploited to execute arbitrary commands, leak sensitive data, or destroy systems.
This lesson covers the most critical security practices that separate a safe script from a dangerous one.
Command injection is the #1 vulnerability in shell scripts. It occurs when user-supplied data is used in a command without proper sanitization.
#!/usr/bin/env bash
# VULNERABLE — NEVER do this
read -p "Enter filename to search: " filename
grep "error" $filename # What if user enters: '; rm -rf /'The user could enter ; rm -rf / and it would execute as:
grep "error" ; rm -rf /# SAFE — always double-quote variable expansions
grep "error" "$filename"With double quotes, the entire value of $filename is treated as a single argument — shell metacharacters inside it have no special meaning.
Never trust user input. Always validate it against a strict whitelist before use.
#!/usr/bin/env bash
read -p "Enter environment (dev/staging/prod): " env
# Whitelist validation — only allow known safe values
case "$env" in
dev|staging|prod) ;; # Valid — continue
*)
echo "Error: Invalid environment '$env'" >&2
exit 1
;;
esac
# Regex validation for structured input
read -p "Enter port number: " port
if [[ ! "$port" =~ ^[0-9]{1,5}$ ]] || (( port < 1 || port > 65535 )); then
echo "Error: '$port' is not a valid port number." >&2
exit 1
fi
# Validate a filename — no slashes, dots at start, or special chars
read -p "Enter filename: " fname
if [[ ! "$fname" =~ ^[a-zA-Z0-9_.-]+$ ]]; then
echo "Error: Filename contains invalid characters." >&2
exit 1
fiYour script should request only the permissions it absolutely needs:
# Check if root is actually required — only then demand it
if [[ $EUID -ne 0 ]] && needs_root; then
echo "Error: This script must be run as root." >&2
exit 1
fi
# Run specific commands as a less-privileged user
sudo -u www-data php artisan cache:clear
# Drop privileges after setup (don't run the whole script as root)
setup_as_root() { ... }
run_as_user() { ... }
setup_as_root
exec sudo -u appuser bash -c "$(declare -f run_as_user); run_as_user"# BAD — secret is visible in the script and process list
DB_PASSWORD="s3cr3tpassword"
mysql -u root -p"$DB_PASSWORD" mydb
# GOOD — read from environment variable
DB_PASSWORD="${DB_PASSWORD:?Error: DB_PASSWORD must be set}"
mysql -u root -p"$DB_PASSWORD" mydb
# BETTER — read from a secure file with restricted permissions
# chmod 600 /etc/myapp/db.conf
source /etc/myapp/db.conf # File contains: DB_PASSWORD="..."Environment variables vs. files:
- ✅ Environment variables are not written to disk.
- ✅ Files with
chmod 600restrict access but leave traces. - ❌ Command-line arguments are visible in
ps auxto any user — never pass secrets this way.
Predictable temporary file names can be exploited (TOCTOU race conditions). Always use mktemp:
# BAD — predictable, exploitable
tmpfile="/tmp/myapp_$$"
# GOOD — unpredictable, atomic creation
tmpfile=$(mktemp) || { echo "Failed to create tmpfile"; exit 1; }
tmpdir=$(mktemp -d)
# Always clean up, even on error
trap 'rm -rf "$tmpfile" "$tmpdir"' EXITShellCheck is a free, open-source tool that finds common bugs and security issues in shell scripts. It is the shell equivalent of a linter.
# Install
apt install shellcheck # Debian/Ubuntu
brew install shellcheck # macOS
# Run against your script
shellcheck myscript.sh
# Common findings:
# SC2086: Double quote to prevent word splitting: echo $var → echo "$var"
# SC2010: Don't use ls | grep: use a glob or find instead
# SC2046: Quote to prevent word splitting: cmd $(subcmd) → cmd "$(subcmd)"
# SC2001: Prefer bash variable operators over sed for simple patterns💡 Run ShellCheck before every commit. Integrate it into your CI/CD pipeline or add a Git pre-commit hook.
| Practice | Why It Matters |
|---|---|
✅ Always quote "$variables" |
Prevents word splitting and injection |
| ✅ Validate all input with a whitelist | Prevents unexpected values from causing harm |
✅ Use set -euo pipefail |
Prevents silent failures from cascading |
✅ Use mktemp for temp files |
Prevents race conditions and predictable path attacks |
| ✅ Never hardcode secrets | Prevents credential exposure in logs/code |
| ✅ Run with minimum privilege | Limits damage if something goes wrong |
✅ Run shellcheck regularly |
Catches common bugs and anti-patterns automatically |
✅ Use readonly for constants |
Prevents accidental modification |
| ✅ Redirect stderr to log file | Prevents sensitive info from leaking to terminal |
Security is not an afterthought — it is designed in from the first line. The three habits that will protect you from 90% of shell script vulnerabilities are:
- Always quote your variables:
"$var"not$var - Validate all input: whitelists over blacklists
- Run ShellCheck: let a tool catch what your eyes miss