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 eval used

  • Variables 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