D Security Best Practices#
Essential security guidelines for writing safe and secure Bash scripts.
1. Input Validation#
Always Validate User Input#
# BAD - No validation
function process_file() {
cat "$1" # What if $1 is malicious?
}
# GOOD - Validate input
function process_file() {
local file="$1"
# Check if provided
if [[ -z "$file" ]]; then
echo "Error: File required" >&2
return 1
fi
# Check if exists and is regular file
if [[ ! -f "$file" ]]; then
echo "Error: Not a valid file: $file" >&2
return 1
fi
# Check permissions (can we read it?)
if [[ ! -r "$file" ]]; then
echo "Error: Cannot read file: $file" >&2
return 1
fi
cat "$file"
}
Validate File Paths#
# Prevent path traversal attacks
function safe_read() {
local file="$1"
local allowed_dir="/var/data"
# Canonicalize path (resolve .. and symlinks)
local real_path
real_path=$(cd "$(dirname "$file")" && pwd)/$(basename "$file")
# Check if within allowed directory
if [[ "$real_path" != "$allowed_dir"* ]]; then
echo "Error: Path outside allowed directory" >&2
return 1
fi
cat "$real_path"
}
Validate Numeric Input#
# Check if argument is a valid number
is_number() {
[[ "$1" =~ ^[0-9]+$ ]]
}
# Usage
if ! is_number "$port"; then
echo "Error: Port must be a number" >&2
exit 1
fi
# More sophisticated validation
is_valid_port() {
local port="$1"
if ! [[ "$port" =~ ^[0-9]+$ ]]; then
return 1
fi
if (( port < 1 || port > 65535 )); then
return 1
fi
return 0
}
2. Command Injection Prevention#
Avoid String Concatenation with User Input#
# DANGEROUS - Command injection vulnerability
query="$1"
result=$(grep "$query" /var/data/file.txt) # If $query contains backticks...
# SAFER - Use grep's pattern argument
result=$(grep -- "$query" /var/data/file.txt) # -- stops option parsing
# Use arrays for complex cases
cmd=(grep -i "$pattern" "$file")
result=$("${cmd[@]}")
Use find with -exec Instead of Piping to Bash#
# DANGEROUS - Can lead to injection
files=$(find . -name "*.txt")
while read -r file; do
command=$(echo "process '$file'")
eval "$command" # NEVER use eval!
done < <(find . -name "*.txt")
# SAFE - Use -exec or -print0
find . -name "*.txt" -type f -exec chmod 644 {} \;
# Process directly without eval
while IFS= read -r -d '' file; do
process "$file"
done < <(find . -name "*.txt" -print0)
Never Use eval#
# DANGEROUS
eval "$user_input" # Arbitrary code execution!
# DANGEROUS
command_to_run="rm -rf $directory" # What if directory has spaces?
eval "$command_to_run"
# SAFE - Use arrays
cmd=(rm -rf "$directory") # Spaces handled correctly
"${cmd[@]}"
# Use functions instead of eval
function run_task() {
local task="$1"
case "$task" in
backup) backup_files ;;
cleanup) cleanup_temp ;;
*) echo "Unknown task: $task" >&2; return 1 ;;
esac
}
3. Quoting and Escaping#
Always Quote Variables#
# DANGEROUS - Word splitting and glob expansion
rm $file # If $file is "important file.txt", deletes two files!
# SAFE - Quote variables
rm "$file" # Correctly removes one file with spaces
# DANGEROUS - Unquoted command substitution
for f in $(ls); do # Breaks on spaces
process "$f"
done
# SAFE - Quote and use arrays
mapfile -t files < <(ls)
for f in "${files[@]}"; do
process "$f"
done
Escape Special Characters#
# When building strings that will be used in commands
filename="user_file.txt"
search_pattern="$1"
# Escape for grep
grep "$(printf '%s\n' "$search_pattern" | sed 's/[[\.*^$()+?{|]/\\&/g')" "$filename"
# Escape for sed
escaped=$(printf '%s\n' "$text" | sed 's:[/\&]:\\&:g')
sed "s/PATTERN/$escaped/g" file.txt
# Escape for find
safe_name=$(printf '%s' "$filename" | sed 's/['"'"']/'\''&'\''/g')
find . -name "$safe_name"
4. File Permissions and Ownership#
Use Proper File Permissions#
# Create files with secure permissions
touch "$config_file"
chmod 600 "$config_file" # Read/write owner only
# Create directories with secure permissions
mkdir "$data_dir"
chmod 700 "$data_dir" # Owner only
# Restrictive default (umask)
umask 077 # Only owner can read/write
# Check before using sensitive files
if [[ -f "$config_file" ]]; then
# Get file permissions in octal
perms=$(stat -c '%a' "$config_file")
# Warn if world-readable
if [[ "${perms:2:1}" != "0" ]]; then
echo "Warning: Config file world-readable: $config_file" >&2
fi
fi
Verify File Ownership#
# Check file owner
check_ownership() {
local file="$1"
local expected_owner="$2"
local actual_owner
actual_owner=$(stat -c '%U' "$file")
if [[ "$actual_owner" != "$expected_owner" ]]; then
echo "Error: $file owned by $actual_owner, not $expected_owner" >&2
return 1
fi
}
# Usage
check_ownership "/etc/important.conf" "root" || exit 1
5. Secrets Management#
Never Hardcode Secrets#
# DANGEROUS - Secret in code
database_password="MySecretPassword123"
# BETTER - Read from configuration file
if [[ -f ~/.config/myapp/secrets ]]; then
source ~/.config/myapp/secrets
fi
# BEST - Use environment variables with secure storage
# Set permissions: chmod 600 ~/.config/myapp/secrets
# In file: export DATABASE_PASSWORD="..."
# Then: source ~/.config/myapp/secrets
database_password="${DATABASE_PASSWORD:-}"
if [[ -z "$database_password" ]]; then
echo "Error: DATABASE_PASSWORD not set" >&2
exit 1
fi
Use Temporary Files Securely#
# Create temporary file securely
tmpfile=$(mktemp)
trap "rm -f '$tmpfile'" EXIT
# Ensure only owner can read
chmod 600 "$tmpfile"
# Use it for sensitive data
echo "$password" > "$tmpfile"
# Process...
cat "$tmpfile"
# Auto-delete on exit (trap above)
Manage SSH Keys Securely#
# Verify SSH key permissions
check_ssh_keys() {
if [[ ! -d ~/.ssh ]]; then
mkdir ~/.ssh
chmod 700 ~/.ssh
fi
if [[ -f ~/.ssh/id_rsa ]]; then
local perms
perms=$(stat -c '%a' ~/.ssh/id_rsa)
if [[ "$perms" != "600" ]]; then
echo "Warning: SSH private key has incorrect permissions: $perms" >&2
chmod 600 ~/.ssh/id_rsa
fi
fi
if [[ -f ~/.ssh/authorized_keys ]]; then
local perms
perms=$(stat -c '%a' ~/.ssh/authorized_keys)
if [[ "$perms" != "600" ]]; then
echo "Warning: authorized_keys has incorrect permissions: $perms" >&2
chmod 600 ~/.ssh/authorized_keys
fi
fi
}
6. Least Privilege#
Run Scripts with Minimal Permissions#
# Create restricted user for script
# sudo useradd -r -s /bin/false appuser
# Use sudo for specific commands
# In /etc/sudoers (use visudo):
# appuser ALL=(root) NOPASSWD: /usr/bin/systemctl restart myservice
# Script runs as appuser, only uses sudo when needed
restart_service() {
sudo /usr/bin/systemctl restart myservice
}
Avoid Using root Unnecessarily#
# Check if root
if [[ $EUID -eq 0 ]]; then
echo "This script should not be run as root" >&2
exit 1
fi
# Or require root when needed
if [[ $EUID -ne 0 ]]; then
echo "This script must be run as root" >&2
exit 1
fi
# Run specific commands with elevated privileges
readonly APP_USER="appuser"
readonly APP_DIR="/opt/myapp"
# Verify permissions
if [[ ! -d "$APP_DIR" ]]; then
sudo mkdir -p "$APP_DIR"
sudo chown "$APP_USER:$APP_USER" "$APP_DIR"
sudo chmod 755 "$APP_DIR"
fi
7. Error Handling and Logging#
Log Security Events#
# Secure logging
log_security_event() {
local event="$1"
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] SECURITY: $event" >> /var/log/security.log
}
# Log failed attempts
log_failed_access() {
local user="$1"
local file="$2"
log_security_event "Failed access by $user to $file"
# Alert on multiple failures
local failures
failures=$(grep "Failed access by $user" /var/log/security.log | wc -l)
if (( failures > 5 )); then
echo "Alert: Multiple failed access attempts by $user" | mail -s "Security Alert" admin@example.com
fi
}
Handle Errors Safely#
# Safe error handling
set -euo pipefail
trap 'log_security_event "Script error at line $LINENO"; exit 1' ERR
trap 'log_security_event "Script interrupted"; exit 130' INT TERM
# Don't expose system internals
if [[ ! -f "$config" ]]; then
echo "Error: Configuration file not found" >&2 # Generic message
# log_security_event "Missing config: $config" # Detailed logging
exit 1
fi
8. Network Security#
Validate Network Input#
# Validate hostname/IP
is_valid_host() {
local host="$1"
# Check if IP or hostname format
if [[ "$host" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
# Validate IP octets
local IFS=.
local -a octets=($host)
for octet in "${octets[@]}"; do
(( octet >= 0 && octet <= 255 )) || return 1
done
elif [[ "$host" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$ ]]; then
return 0 # Valid hostname
else
return 1
fi
}
# Use SSH with security options
ssh_remote_command() {
local host="$1"
local cmd="$2"
# Validate host
is_valid_host "$host" || return 1
# Use security options
ssh -o ConnectTimeout=5 \
-o StrictHostKeyChecking=accept-new \
-o UserKnownHostsFile=/dev/null \
-o IdentitiesOnly=yes \
-i ~/.ssh/id_rsa \
"$host" "$cmd"
}
9. Regular Auditing#
Audit Permissions#
# Check for world-writable files
audit_permissions() {
echo "World-writable files:"
find /home -type f -perm -002 2>/dev/null
echo "Files with SUID bit:"
find /home -type f -perm -4000 2>/dev/null
echo "Config files with incorrect permissions:"
find /etc -type f ! -perm -644 2>/dev/null | head -20
}
Regular Security Updates#
# Check for available updates
check_updates() {
echo "Checking for security updates..."
apt-get update
apt-get -s upgrade | grep -i security
}
# Log all sudo usage
# In /etc/sudoers.d/logging:
# Defaults logfile="/var/log/sudo.log"
10. Secure Coding Checklist#
All user input validated
No
evalusedVariables quoted when used
Sensitive data not in code
File permissions checked
No world-writable files created
Error messages don’t leak info
Secrets never in logs
SSL/TLS for network communication
Authentication validated
Authorization checked
Race conditions avoided
Temporary files created securely
Signal handlers implemented
Tested with untrusted input