Table of Contents
- Common Vulnerabilities in Bash Scripts
- Best Practices for Secure Bash Scripting
- Conclusion
- References
Common Vulnerabilities in Bash Scripts
1.1 Command Injection
What is it?
Command injection occurs when untrusted input is passed directly to a bash command, allowing an attacker to execute arbitrary code. This is one of the most dangerous vulnerabilities in bash scripts, often caused by improper sanitization of user input or dynamic variables.
Insecure Example:
Suppose you write a script to delete a user-specified file:
#!/bin/bash
echo "Enter file to delete:"
read filename
rm -rf $filename # UNSAFE!
If an attacker enters file.txt; rm -rf /, the script will execute rm -rf file.txt; rm -rf /—deleting the target file and attempting to wipe the entire filesystem.
Secure Alternative:
Sanitize input and use quotes to prevent arbitrary command execution. Validate that the input is a safe filename before acting:
#!/bin/bash
echo "Enter file to delete (relative path only):"
read filename
# Validate input: allow only letters, numbers, dots, hyphens, and underscores
if [[ ! "$filename" =~ ^[a-zA-Z0-9._-]+$ ]]; then
echo "Error: Invalid filename."
exit 1
fi
# Use quotes to prevent word splitting and command injection
rm -rf "$filename"
Why It Works:
- Input validation with a regex (
^[a-zA-Z0-9._-]+$) restricts filenames to safe characters. - Quoting
"$filename"ensures bash treats it as a single argument, preventing injection of additional commands.
1.2 Insecure Variable Handling
What is it?
Bash variables are prone to unexpected behavior if not handled carefully. Undefined variables, unquoted variables, or variables containing special characters (e.g., spaces, *, ;) can lead to errors, data corruption, or security holes.
Insecure Example:
#!/bin/bash
# User input with spaces (e.g., "my file.txt")
read -p "Enter filename: " filename
# Unquoted variable: bash splits "my file.txt" into "my" and "file.txt"
ls $filename # Fails: "ls: cannot access 'my': No such file or directory"
Secure Alternative:
#!/bin/bash
# Exit on undefined variables (-u) and errors (-e)
set -eu
read -p "Enter filename: " filename
# Quoted variable: preserves spaces and special characters
ls "$filename"
Why It Works:
set -u(nounset) exits the script if an undefined variable is used, preventing silent failures.- Quoting
"$filename"ensures the variable is treated as a single argument, even with spaces or special characters.
1.3 Improper File Handling
What is it?
Scripts that read/write files without validating file types, checking permissions, or resolving symlinks are vulnerable to attacks. For example, a malicious user could create a symlink to /etc/passwd in a directory your script writes to, leading to data leaks.
Insecure Example:
#!/bin/bash
# Overwrite a "log file" without checking if it's a symlink
log_file="/tmp/app.log"
echo "Sensitive data" > "$log_file" # If /tmp/app.log is a symlink to /etc/passwd, this overwrites it!
Secure Alternative:
#!/bin/bash
set -eu
log_file="/tmp/app.log"
# Check if the file is a symlink (avoid symlink attacks)
if [ -L "$log_file" ]; then
echo "Error: $log_file is a symlink. Aborting."
exit 1
fi
# Check if the file exists and is writable (or create it safely)
if [ -f "$log_file" ] && [ ! -w "$log_file" ]; then
echo "Error: $log_file is not writable."
exit 1
fi
# Write to the file safely
echo "Sensitive data" > "$log_file"
Why It Works:
[ -L "$log_file" ]checks if the file is a symlink, blocking symlink attacks.[ -f "$log_file" ] && [ ! -w "$log_file" ]ensures the script only writes to files it has permission to modify.
1.4 Lack of Error Checking
What is it?
By default, bash continues executing scripts even if a command fails (e.g., rm non_existent_file returns an error code but doesn’t stop the script). This can lead to cascading failures, data corruption, or incomplete workflows.
Insecure Example:
#!/bin/bash
# Script proceeds even if "backup" fails
tar -czf /backup/data.tar.gz /data
# If tar fails (e.g., /data is missing), the script still deletes the source!
rm -rf /data
Secure Alternative:
#!/bin/bash
# Exit on error (-e) and pipeline failures (-o pipefail)
set -eo pipefail
tar -czf /backup/data.tar.gz /data || {
echo "Error: Backup failed. Aborting deletion."
exit 1
}
# Only delete /data if backup succeeded
rm -rf /data
Why It Works:
set -e(errexit) exits the script if any command fails.set -o pipefailensures pipelines (e.g.,cmd1 | cmd2) fail if any command in the pipeline fails (not just the last one).- Explicit error handling with
|| { ... }provides granular control over failure scenarios.
1.5 Insecure Temporary Files
What is it?
Temporary files stored in /tmp (or other world-writable directories) are vulnerable to symlink attacks. An attacker can create a symlink with the same name as your temp file before your script runs, redirecting writes to sensitive locations (e.g., /etc/shadow).
Insecure Example:
#!/bin/bash
# Hardcoded temp file name (predictable and vulnerable)
temp_file="/tmp/temp_data.txt"
echo "Sensitive data" > "$temp_file"
Secure Alternative:
Use mktemp to generate a unique, secure temporary file:
#!/bin/bash
set -eu
# Create a unique temp file (mktemp returns a secure, random name)
temp_file=$(mktemp) || { echo "Error: Failed to create temp file."; exit 1; }
# Clean up the temp file on exit (even if the script crashes)
trap 'rm -f "$temp_file"' EXIT
# Write to the temp file safely
echo "Sensitive data" > "$temp_file"
Why It Works:
mktempgenerates a unique filename (e.g.,/tmp/tmp.XXXXXXXXXX) that’s hard to guess, preventing symlink attacks.trap 'rm -f "$temp_file"' EXITensures the temp file is deleted when the script exits, even if it crashes.
1.6 Hardcoded Secrets
What is it?
Scripts often include hardcoded passwords, API keys, or SSH keys for convenience. This is a critical risk: anyone with read access to the script can steal these secrets, leading to unauthorized access to databases, cloud services, or servers.
Insecure Example:
#!/bin/bash
# Hardcoded database credentials (exposed to anyone who reads the script)
DB_USER="admin"
DB_PASS="SecurePassword123!"
mysql -u "$DB_USER" -p"$DB_PASS" -e "SELECT * FROM users;"
Secure Alternative:
Store secrets in environment variables or use a secure vault:
#!/bin/bash
set -eu
# Load secrets from environment variables (never hardcode!)
DB_USER="${DB_USER:?Error: DB_USER environment variable not set.}"
DB_PASS="${DB_PASS:?Error: DB_PASS environment variable not set.}"
mysql -u "$DB_USER" -p"$DB_PASS" -e "SELECT * FROM users;"
Why It Works:
- Secrets are injected via environment variables (e.g.,
export DB_PASS="..."), keeping them out of the script’s source code. ${DB_PASS:?Error: ...}ensures the script exits if the variable is undefined, preventing accidental execution with missing secrets.
1.7 Excessive Privileges & Permission Issues
What is it?
Scripts running with unnecessary privileges (e.g., as root) or with overly permissive file permissions (e.g., chmod 777)扩大 the attack surface. A vulnerability in a root-run script can compromise the entire system.
Insecure Example:
#!/bin/bash
# Run as root to modify /etc/hosts (unnecessary if the script only needs write access to /etc/hosts)
echo "192.168.1.100 example.com" >> /etc/hosts
Secure Alternative:
- Use the principle of least privilege: run the script with the minimal permissions required.
- Restrict file permissions to
chmod 700(only the owner can read/write/execute).
#!/bin/bash
set -eu
# Check if the user has write permission to /etc/hosts (instead of running as root)
if [ ! -w "/etc/hosts" ]; then
echo "Error: No write permission to /etc/hosts. Run with sudo if needed."
exit 1
fi
echo "192.168.1.100 example.com" >> /etc/hosts
Why It Works:
- Checking
[ -w "/etc/hosts" ]ensures the script only proceeds if the user has explicit write access, avoiding unnecessary root privileges. - Setting script permissions to
chmod 700 script.shprevents other users from reading or modifying the script.
Best Practices for Secure Bash Scripting
To summarize, here are key habits to secure your bash scripts:
-
Enable Strict Mode: Start scripts with
set -euo pipefailto exit on errors, undefined variables, and pipeline failures.#!/bin/bash set -euo pipefail # Strict mode: exit on errors, undefined vars, and pipeline failures -
Sanitize All Inputs: Validate user input with regex, allowlists, or tools like
printf "%q" "$var"to escape special characters. -
Avoid
eval:evalexecutes arbitrary code and is a common vector for command injection. Use alternatives like functions or parameter expansion. -
Use
mktempfor Temp Files: Never hardcode temp file names—mktempensures uniqueness and security. -
Scan with
shellcheck: Use the ShellCheck linter to automatically detect vulnerabilities (e.g., unquoted variables, insecure temp files).shellcheck your_script.sh # Flags issues like unquoted variables or missing error checks -
Test with Malicious Inputs: Simulate attacks (e.g., input with
; rm -rf /, symlinks, or special characters) to validate your script’s resilience.
Conclusion
Bash scripts are powerful, but their simplicity can hide dangerous vulnerabilities. By addressing command injection, insecure variables, improper file handling, and other common issues, you can significantly reduce your attack surface. Adopting strict mode, sanitizing inputs, and using tools like mktemp and shellcheck will help you build robust, secure automation.
Remember: security is a mindset, not a one-time check. Regularly audit and update your scripts, and treat them with the same care as production code. Your systems (and your sanity) will thank you.