Posted: November 4, 2024
Author: CrisisCore-Systems
Tags: Security, Bash, Shell Scripting, Best Practices, DevSecOps
It was 2:47 AM when the alerts started flooding in. A popular web service had been compromised through what seemed like an innocuous vulnerability—a shell script that processed user-uploaded files. The script, written hastily during a late-night deployment six months earlier, had a single line that would prove catastrophic:
process_file() {
filename="$1"
cat "/var/data/$filename" > output.txt
}A malicious user discovered they could pass ../../../etc/passwd as the filename, exposing the server's user database. Within hours, the breach expanded—attackers used similar path traversal attacks to access SSH keys, database credentials, and customer data. The company's stock price plummeted 40% the next day. The total cost: $8 million in direct damages, countless hours of remediation, and irreparable damage to their reputation.
This isn't a cautionary tale from some distant past—variations of this story play out regularly in organizations of all sizes. The reality is sobering: shell scripts, often dismissed as "quick and dirty" solutions, handle sensitive operations in production environments worldwide. They manage backups, process uploads, manipulate files, and interact with system resources—all potential attack vectors.
"Shell scripts don't need security" is a dangerous misconception that costs organizations countless hours debugging production failures, investigating security incidents, and dealing with data corruption. Many developers treat shell scripts as throwaway code that doesn't warrant the same security rigor as compiled applications. This casual approach leads to:
- Silent failures that go unnoticed until it's too late
- Cascading errors that corrupt critical data
- Security vulnerabilities that expose entire systems
- Compliance violations with severe legal consequences
- Difficult-to-debug issues that surface only in production
CompressKit demonstrates that enterprise-grade security in shell scripts is not only possible but essential. With comprehensive security measures that go beyond typical shell script projects, CompressKit shows how to build defensive, resilient code that stands up to real-world attacks.
In this post, we'll explore the security patterns and practices that make CompressKit robust against common attack vectors—patterns you can apply to your own shell scripts to prevent the next 2:47 AM disaster.
Shell scripts present unique security challenges:
- String-Based Execution: Everything is ultimately a string, making injection attacks trivial
- Path Manipulation: File operations are vulnerable to path traversal
- Wildcard Expansion: Unquoted variables can lead to unexpected behavior
- Subprocess Spawning: Commands are executed in subshells with inherited environments
- Limited Type Safety: No built-in type checking or validation
CompressKit addresses these challenges through a multi-layered security architecture.
CompressKit implements security through four primary layers:
┌─────────────────────────────────────────┐
│ Input Validation Layer │
│ - Type checking │
│ - Range validation │
│ - Allowlist enforcement │
└──────────────┬──────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ Path Security Layer │
│ - Traversal prevention │
│ - Symbolic link validation │
│ - Sensitive path protection │
└──────────────┬──────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ Command Execution Layer │
│ - Allowlist-based execution │
│ - Argument validation │
│ - No eval usage │
└──────────────┬──────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ Audit and Response Layer │
│ - Security incident logging │
│ - Admin notifications │
│ - Audit trail maintenance │
└─────────────────────────────────────────┘
Let's examine each layer in detail.
Consider this vulnerable code that might look familiar—it's the kind of code written under deadline pressure, committed late at night:
# VULNERABLE CODE - DO NOT USE
compress_file() {
local quality="$1"
local file="$2"
# Direct use of user input
gs -dQuality=$quality -o output.pdf $file
}"It's just a simple compression script," the developer thinks. "What could go wrong?"
Everything. This innocuous-looking function has at least three critical vulnerabilities:
- Command Injection via Quality Parameter: An attacker passes
"high; rm -rf /"as the quality parameter - Arbitrary File Access: The file parameter could be
/etc/shadowor any sensitive system file - Wildcard Expansion: If the file variable contains
*, it could expand to multiple files, causing unexpected behavior
In 2019, a similar vulnerability in a backup script at a financial services company allowed an attacker to exfiltrate customer data worth millions. The script had been in production for three years before anyone noticed the vulnerability—and by then, it was too late.
This is why input validation isn't optional—it's survival.
CompressKit implements a robust validation function:
validate_input() {
local value="$1"
shift
local allowed_values=("$@")
# Check if value matches any allowed value
for allowed in "${allowed_values[@]}"; do
if [ "$value" = "$allowed" ]; then
return 0
fi
done
return 1
}Usage in practice:
compress_pdf() {
local quality="$1"
# Validate against allowlist
if ! validate_input "$quality" "high" "medium" "low" "ultra"; then
log_error "Invalid quality level: $quality"
return 1
fi
# Safe to proceed with validated input
# ...
}- Allowlist, Never Blocklist: Define what's allowed, not what's forbidden
- Validate Early: Check inputs before any processing
- Fail Securely: Reject invalid input and log the attempt
- No Assumptions: Even "internal" functions should validate inputs
Path traversal is one of the most common vulnerabilities in file-handling scripts. In 2021, a healthcare provider discovered that their patient record export script had a path traversal vulnerability. An attacker had been accessing arbitrary files for months, including database credentials and encryption keys. The breach affected 2.3 million patients and resulted in a $4.5 million fine.
The vulnerable code looked innocent enough:
# VULNERABLE CODE - DO NOT USE
process_file() {
local filename="$1"
cat "/var/data/$filename" > output.txt
}
# Attack: process_file "../../../etc/passwd""But we sanitize the input on the frontend," the developers insisted. They learned the hard way that client-side validation is not security—it's a suggestion that attackers ignore.
Path traversal attacks are insidious because they're so simple. No sophisticated exploit code, no buffer overflows, just a few dots and slashes in the right place. Yet they can expose your entire filesystem to attackers.
CompressKit implements a comprehensive path validation function:
safe_path() {
local path="$1"
local security_level="${2:-medium}"
# 1. Reject null bytes
if [[ "$path" =~ $'\0' ]]; then
log_error "Path contains null byte"
return 1
fi
# 2. Reject URL-encoded traversal attempts
if [[ "$path" =~ %2e%2e|%2E%2E|%2f|%2F ]]; then
log_error "URL-encoded path traversal attempt detected"
return 1
fi
# 3. Reject obvious path traversal patterns
if [[ "$path" =~ \.\./|\.\.\\ ]]; then
log_error "Path traversal attempt detected"
return 1
fi
# 4. Normalize path (resolves symlinks, removes ..)
local normalized_path
normalized_path=$(realpath -m "$path" 2>/dev/null)
if [ $? -ne 0 ]; then
log_error "Failed to normalize path"
return 1
fi
# 5. Validate against sensitive paths
local sensitive_paths=(
"/etc/shadow"
"/etc/passwd"
"/root"
"/var/log"
)
if [ "$security_level" = "high" ]; then
for sensitive in "${sensitive_paths[@]}"; do
if [[ "$normalized_path" =~ ^"$sensitive" ]]; then
log_error "Access to sensitive path denied"
return 1
fi
done
fi
# 6. Check if it's a symbolic link to a sensitive location
if [ -L "$path" ]; then
local link_target
link_target=$(readlink -f "$path")
for sensitive in "${sensitive_paths[@]}"; do
if [[ "$link_target" =~ ^"$sensitive" ]]; then
log_error "Symbolic link to sensitive path denied"
return 1
fi
done
fi
# Return normalized path
echo "$normalized_path"
return 0
}process_file_secure() {
local input_file="$1"
# Validate path
local safe_file
safe_file=$(safe_path "$input_file" "high")
if [ $? -ne 0 ] || [ -z "$safe_file" ]; then
log_error "Invalid file path"
return 1
fi
# Additional checks
if [ ! -f "$safe_file" ]; then
log_error "File does not exist"
return 1
fi
if [ ! -r "$safe_file" ]; then
log_error "File is not readable"
return 1
fi
# Now safe to process
cat "$safe_file" > output.txt
}The safe_path() function implements multiple layers of defense:
- Input Sanitization: Removes dangerous characters
- Pattern Matching: Detects traversal attempts
- Normalization: Resolves to absolute paths
- Allowlist Checking: Validates against known-good locations
- Symbolic Link Validation: Checks link targets
- Existence Validation: Confirms file exists and is accessible
The most dangerous construct in shell scripting is eval:
# VULNERABLE CODE - DO NOT USE
execute_command() {
local cmd="$1"
eval "$cmd" # NEVER DO THIS
}
# Attack: execute_command "rm -rf /"CompressKit's safe_execute() function implements allowlist-based command execution:
safe_execute() {
local command="$1"
# Parse command to extract the base command
local base_cmd
base_cmd=$(echo "$command" | awk '{print $1}')
# Allowlist of permitted commands
local allowed_commands=(
"gs" # GhostScript
"qpdf" # QPDF tool
"convert" # ImageMagick
"identify" # ImageMagick identify
"pdfinfo" # PDF info tool
"grep" # Text search
"cat" # File concatenation
"mkdir" # Directory creation
"chmod" # Permission change
)
# Check if command is in allowlist
local allowed=false
for cmd in "${allowed_commands[@]}"; do
if [ "$base_cmd" = "$cmd" ]; then
allowed=true
break
fi
done
if [ "$allowed" = false ]; then
log_error "Command not in allowlist: $base_cmd"
return 1
fi
# Execute with explicit bash -c to control environment
bash -c "$command"
return $?
}- No eval: Never uses dangerous dynamic evaluation
- Allowlist Only: Only explicitly permitted commands execute
- Controlled Environment: Executes in a controlled subshell
- Logged Attempts: All execution attempts are logged
CompressKit maintains detailed logs of security-relevant events:
log_security_incident() {
local incident_type="$1"
local details="$2"
local severity="${3:-medium}"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
local incident_log="${HOME}/.config/compresskit/security.log"
# Create log entry
local log_entry="[${timestamp}] [${severity}] ${incident_type}: ${details}"
# Write to security log
echo "$log_entry" >> "$incident_log"
# Also log to syslog if available
if command -v logger &>/dev/null; then
logger -t compresskit -p auth.warning "$log_entry"
fi
# Send notification for high-severity incidents
if [ "$severity" = "high" ] || [ "$severity" = "critical" ]; then
notify_admin "Security Incident" "$log_entry"
fi
}notify_admin() {
local subject="$1"
local message="$2"
# Multiple notification channels
# 1. Email notification (if configured)
if [ -n "$ADMIN_EMAIL" ] && command -v mail &>/dev/null; then
echo "$message" | mail -s "$subject" "$ADMIN_EMAIL"
fi
# 2. Log to system
log_error "[ADMIN ALERT] $subject: $message"
# 3. Create alert file
local alert_file="${HOME}/.config/compresskit/alerts/$(date +%Y%m%d_%H%M%S).alert"
mkdir -p "$(dirname "$alert_file")"
echo "$message" > "$alert_file"
chmod 600 "$alert_file"
}secure_tempfile() {
local prefix="${1:-compresskit}"
# Use mktemp for secure temp file creation
local temp_file
temp_file=$(mktemp -t "${prefix}.XXXXXXXXXX")
if [ $? -ne 0 ]; then
log_error "Failed to create secure temporary file"
return 1
fi
# Set restrictive permissions
chmod 600 "$temp_file"
# Register for cleanup
register_cleanup "$temp_file"
echo "$temp_file"
return 0
}atomic_write() {
local target_file="$1"
local content="$2"
# Validate target path
local safe_target
safe_target=$(safe_path "$target_file")
if [ $? -ne 0 ]; then
return 1
fi
# Create secure temp file
local temp_file
temp_file=$(secure_tempfile "atomic")
# Write to temp file
echo "$content" > "$temp_file"
# Atomic move
mv -f "$temp_file" "$safe_target"
local result=$?
# Set appropriate permissions
chmod 600 "$safe_target"
return $result
}CompressKit implements license verification using OpenSSL:
verify_license_signature() {
local license_file="$1"
local signature_file="$2"
local public_key_file="$3"
# Validate all paths
local safe_license safe_sig safe_key
safe_license=$(safe_path "$license_file") || return 1
safe_sig=$(safe_path "$signature_file") || return 1
safe_key=$(safe_path "$public_key_file") || return 1
# Verify file existence
[ -f "$safe_license" ] || return 1
[ -f "$safe_sig" ] || return 1
[ -f "$safe_key" ] || return 1
# Perform signature verification
if command -v openssl &>/dev/null; then
openssl dgst -sha256 -verify "$safe_key" \
-signature "$safe_sig" "$safe_license" &>/dev/null
return $?
else
log_warning "OpenSSL not available for signature verification"
return 1
fi
}This ensures:
- License files haven't been tampered with
- Only validly signed licenses are accepted
- Cryptographic proof of authenticity
CompressKit includes comprehensive security tests:
test_path_traversal_prevention() {
echo "Testing path traversal prevention..."
local attack_patterns=(
"../../../etc/passwd"
"..\\..\\..\\windows\\system32\\config\\sam"
"/etc/../etc/passwd"
"%2e%2e%2f%2e%2e%2fetc%2fpasswd"
"file\x00.txt"
)
for pattern in "${attack_patterns[@]}"; do
if safe_path "$pattern" "high" &>/dev/null; then
echo "FAIL: Pattern '$pattern' was not blocked"
return 1
fi
done
echo "PASS: All path traversal attempts were blocked"
return 0
}Based on CompressKit's implementation, here are key security practices for shell scripts:
- ✅ Always validate against allowlists
- ✅ Check types and ranges
- ✅ Validate early, fail securely
- ❌ Never trust user input
- ✅ Use
safe_path()for all file operations - ✅ Normalize paths with
realpath - ✅ Validate symbolic links
- ❌ Never concatenate user input into paths
- ✅ Use allowlist-based execution
- ✅ Avoid
evalentirely - ✅ Quote all variables
- ❌ Never execute user-provided commands directly
- ✅ Use secure temp file creation (
mktemp) - ✅ Set restrictive permissions (600/700)
- ✅ Implement atomic operations
- ❌ Never leave temp files unprotected
- ✅ Log security incidents
- ✅ Provide user-friendly messages
- ✅ Implement admin notifications
- ❌ Never expose sensitive information in errors
- ✅ Use established tools (OpenSSL)
- ✅ Verify signatures
- ✅ Validate expiration dates
- ❌ Never implement custom crypto
To implement these patterns in your own scripts:
- Start with the security module: Create a
security.shfile withsafe_path()andvalidate_input() - Wrap all file operations: Never access files without path validation
- Implement allowlist-based execution: Create a
safe_execute()function for your use case - Add comprehensive logging: Track all security-relevant events
- Test thoroughly: Include security test cases in your test suite
Security in shell scripts is not optional—it's essential. CompressKit demonstrates that with proper design and implementation, shell scripts can be as secure as any other application.
The patterns and functions presented here are production-ready and can be adapted to your own projects. By implementing these security measures, you can:
- Prevent common vulnerabilities
- Maintain audit trails
- Respond to security incidents
- Build user trust
- Meet compliance requirements
Remember: security is not a feature—it's a fundamental requirement.
Resources:
About the Author:
CrisisCore-Systems specializes in secure software development and open-source tooling. We believe security should be accessible and understandable for all developers.