Initial commit
This commit is contained in:
582
skills/security-first-2025.md
Normal file
582
skills/security-first-2025.md
Normal file
@@ -0,0 +1,582 @@
|
||||
---
|
||||
name: security-first-2025
|
||||
description: Security-first bash scripting patterns for 2025 (mandatory validation, zero-trust)
|
||||
---
|
||||
|
||||
## 🚨 CRITICAL GUIDELINES
|
||||
|
||||
### Windows File Path Requirements
|
||||
|
||||
**MANDATORY: Always Use Backslashes on Windows for File Paths**
|
||||
|
||||
When using Edit or Write tools on Windows, you MUST use backslashes (`\`) in file paths, NOT forward slashes (`/`).
|
||||
|
||||
**Examples:**
|
||||
- ❌ WRONG: `D:/repos/project/file.tsx`
|
||||
- ✅ CORRECT: `D:\repos\project\file.tsx`
|
||||
|
||||
This applies to:
|
||||
- Edit tool file_path parameter
|
||||
- Write tool file_path parameter
|
||||
- All file operations on Windows systems
|
||||
|
||||
|
||||
### Documentation Guidelines
|
||||
|
||||
**NEVER create new documentation files unless explicitly requested by the user.**
|
||||
|
||||
- **Priority**: Update existing README.md files rather than creating new documentation
|
||||
- **Repository cleanliness**: Keep repository root clean - only README.md unless user requests otherwise
|
||||
- **Style**: Documentation should be concise, direct, and professional - avoid AI-generated tone
|
||||
- **User preference**: Only create additional .md files when user specifically asks for documentation
|
||||
|
||||
|
||||
---
|
||||
|
||||
# Security-First Bash Scripting (2025)
|
||||
|
||||
## Overview
|
||||
|
||||
2025 security assessments reveal **60%+ of exploited automation tools lacked adequate input sanitization**. This skill provides mandatory security patterns.
|
||||
|
||||
## Critical Security Patterns
|
||||
|
||||
### 1. Input Validation (Non-Negotiable)
|
||||
|
||||
**Every input MUST be validated before use:**
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# ✅ REQUIRED: Validate all inputs
|
||||
validate_input() {
|
||||
local input="$1"
|
||||
local pattern="$2"
|
||||
local max_length="${3:-255}"
|
||||
|
||||
# Check empty
|
||||
if [[ -z "$input" ]]; then
|
||||
echo "Error: Input required" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check pattern
|
||||
if [[ ! "$input" =~ $pattern ]]; then
|
||||
echo "Error: Invalid format" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check length
|
||||
if [[ ${#input} -gt $max_length ]]; then
|
||||
echo "Error: Input too long (max $max_length)" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Usage
|
||||
read -r user_input
|
||||
if validate_input "$user_input" '^[a-zA-Z0-9_-]+$' 50; then
|
||||
process "$user_input"
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
### 2. Command Injection Prevention
|
||||
|
||||
**NEVER use eval or dynamic execution with user input:**
|
||||
|
||||
```bash
|
||||
# ❌ DANGEROUS - Command injection vulnerability
|
||||
user_input="$(cat user_file.txt)"
|
||||
eval "$user_input" # NEVER DO THIS
|
||||
|
||||
# ❌ DANGEROUS - Indirect command injection
|
||||
grep "$user_pattern" file.txt # If pattern is "-e /etc/passwd"
|
||||
|
||||
# ✅ SAFE - Use -- separator
|
||||
grep -- "$user_pattern" file.txt
|
||||
|
||||
# ✅ SAFE - Use arrays
|
||||
grep_args=("$user_pattern" "file.txt")
|
||||
grep "${grep_args[@]}"
|
||||
|
||||
# ✅ SAFE - Validate before use
|
||||
if [[ "$user_pattern" =~ ^[a-zA-Z0-9]+$ ]]; then
|
||||
grep "$user_pattern" file.txt
|
||||
fi
|
||||
```
|
||||
|
||||
### 3. Path Traversal Prevention
|
||||
|
||||
**Sanitize and validate ALL file paths:**
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Sanitize path components
|
||||
sanitize_path() {
|
||||
local path="$1"
|
||||
|
||||
# Remove dangerous patterns
|
||||
path="${path//..\/}" # Remove ../
|
||||
path="${path//\/..\//}" # Remove /../
|
||||
path="${path#/}" # Remove leading /
|
||||
|
||||
echo "$path"
|
||||
}
|
||||
|
||||
# Validate path is within allowed directory
|
||||
is_safe_path() {
|
||||
local file_path="$1"
|
||||
local base_dir="$2"
|
||||
|
||||
# Resolve to absolute paths
|
||||
local real_path real_base
|
||||
real_path=$(readlink -f "$file_path" 2>/dev/null) || return 1
|
||||
real_base=$(readlink -f "$base_dir" 2>/dev/null) || return 1
|
||||
|
||||
# Check path starts with base
|
||||
[[ "$real_path" == "$real_base"/* ]]
|
||||
}
|
||||
|
||||
# Usage
|
||||
user_file=$(sanitize_path "$user_input")
|
||||
if is_safe_path "/var/app/uploads/$user_file" "/var/app/uploads"; then
|
||||
cat "/var/app/uploads/$user_file"
|
||||
else
|
||||
echo "Error: Access denied" >&2
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
### 4. Secure Temporary Files
|
||||
|
||||
**Never use predictable temp file names:**
|
||||
|
||||
```bash
|
||||
# ❌ DANGEROUS - Race condition vulnerability
|
||||
temp_file="/tmp/myapp.tmp"
|
||||
echo "data" > "$temp_file" # Can be symlinked by attacker
|
||||
|
||||
# ❌ DANGEROUS - Predictable name
|
||||
temp_file="/tmp/myapp-$$.tmp" # PID can be guessed
|
||||
|
||||
# ✅ SAFE - Use mktemp
|
||||
temp_file=$(mktemp)
|
||||
chmod 600 "$temp_file" # Owner-only permissions
|
||||
echo "data" > "$temp_file"
|
||||
|
||||
# ✅ SAFE - Automatic cleanup
|
||||
readonly TEMP_FILE=$(mktemp)
|
||||
trap 'rm -f "$TEMP_FILE"' EXIT INT TERM
|
||||
|
||||
# ✅ SAFE - Temp directory
|
||||
readonly TEMP_DIR=$(mktemp -d)
|
||||
trap 'rm -rf "$TEMP_DIR"' EXIT INT TERM
|
||||
chmod 700 "$TEMP_DIR"
|
||||
```
|
||||
|
||||
### 5. Secrets Management
|
||||
|
||||
**NEVER hardcode secrets or expose them:**
|
||||
|
||||
```bash
|
||||
# ❌ DANGEROUS - Hardcoded secrets
|
||||
DB_PASSWORD="supersecret123"
|
||||
|
||||
# ❌ DANGEROUS - Secrets in environment (visible in ps)
|
||||
export DB_PASSWORD="supersecret123"
|
||||
|
||||
# ✅ SAFE - Read from secure file
|
||||
if [[ -f /run/secrets/db_password ]]; then
|
||||
DB_PASSWORD=$(< /run/secrets/db_password)
|
||||
chmod 600 /run/secrets/db_password
|
||||
else
|
||||
echo "Error: Secret not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ✅ SAFE - Use cloud secret managers
|
||||
get_secret() {
|
||||
local secret_name="$1"
|
||||
|
||||
# AWS Secrets Manager
|
||||
aws secretsmanager get-secret-value \
|
||||
--secret-id "$secret_name" \
|
||||
--query SecretString \
|
||||
--output text
|
||||
}
|
||||
|
||||
DB_PASSWORD=$(get_secret "production/database/password")
|
||||
|
||||
# ✅ SAFE - Prompt for sensitive data (no echo)
|
||||
read -rsp "Enter password: " password
|
||||
echo # Newline after password
|
||||
```
|
||||
|
||||
### 6. Privilege Management
|
||||
|
||||
**Follow least privilege principle:**
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Check not running as root
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
echo "Error: Do not run as root" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Drop privileges if started as root
|
||||
drop_privileges() {
|
||||
local target_user="$1"
|
||||
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
echo "Dropping privileges to $target_user" >&2
|
||||
exec sudo -u "$target_user" "$0" "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# Run specific command with minimal privileges
|
||||
run_privileged() {
|
||||
local command="$1"
|
||||
shift
|
||||
|
||||
# Use sudo with minimal scope
|
||||
sudo --non-interactive \
|
||||
--reset-timestamp \
|
||||
"$command" "$@"
|
||||
}
|
||||
|
||||
# Usage
|
||||
drop_privileges "appuser"
|
||||
```
|
||||
|
||||
### 7. Environment Variable Sanitization
|
||||
|
||||
**Clean environment before executing:**
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Clean environment
|
||||
clean_environment() {
|
||||
# Unset dangerous variables
|
||||
unset IFS
|
||||
unset CDPATH
|
||||
unset GLOBIGNORE
|
||||
|
||||
# Set safe PATH (absolute paths only)
|
||||
export PATH="/usr/local/bin:/usr/bin:/bin"
|
||||
|
||||
# Set safe IFS
|
||||
IFS=$'\n\t'
|
||||
}
|
||||
|
||||
# Execute command in clean environment
|
||||
exec_clean() {
|
||||
env -i \
|
||||
HOME="$HOME" \
|
||||
USER="$USER" \
|
||||
PATH="/usr/local/bin:/usr/bin:/bin" \
|
||||
"$@"
|
||||
}
|
||||
|
||||
# Usage
|
||||
clean_environment
|
||||
exec_clean /usr/local/bin/myapp
|
||||
```
|
||||
|
||||
### 8. Absolute Path Usage (2025 Best Practice)
|
||||
|
||||
**Always use absolute paths to prevent PATH hijacking:**
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# ❌ DANGEROUS - Vulnerable to PATH manipulation
|
||||
curl https://example.com/data
|
||||
jq '.items[]' data.json
|
||||
|
||||
# ✅ SAFE - Absolute paths
|
||||
/usr/bin/curl https://example.com/data
|
||||
/usr/bin/jq '.items[]' data.json
|
||||
|
||||
# ✅ SAFE - Verify command location
|
||||
CURL=$(command -v curl) || { echo "curl not found" >&2; exit 1; }
|
||||
"$CURL" https://example.com/data
|
||||
```
|
||||
|
||||
**Why This Matters:**
|
||||
- Prevents malicious binaries in user PATH
|
||||
- Standard practice in enterprise environments
|
||||
- Required for security-sensitive scripts
|
||||
|
||||
### 9. History File Protection (2025 Security)
|
||||
|
||||
**Disable history for credential operations:**
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Disable history for this session
|
||||
HISTFILE=/dev/null
|
||||
export HISTFILE
|
||||
|
||||
# Or disable specific commands
|
||||
HISTIGNORE="*password*:*secret*:*token*"
|
||||
export HISTIGNORE
|
||||
|
||||
# Handle sensitive operations
|
||||
read -rsp "Enter database password: " db_password
|
||||
echo
|
||||
|
||||
# Use password (not logged to history)
|
||||
/usr/bin/mysql -p"$db_password" -e "SELECT 1"
|
||||
|
||||
# Clear variable
|
||||
unset db_password
|
||||
```
|
||||
|
||||
## Security Checklist (2025)
|
||||
|
||||
Every script MUST pass these checks:
|
||||
|
||||
### Input Validation
|
||||
- [ ] All user inputs validated with regex patterns
|
||||
- [ ] Maximum length enforced on all inputs
|
||||
- [ ] Empty/null inputs rejected
|
||||
- [ ] Special characters escaped or rejected
|
||||
|
||||
### Command Safety
|
||||
- [ ] No eval with user input
|
||||
- [ ] No dynamic variable names from user input
|
||||
- [ ] All command arguments use -- separator
|
||||
- [ ] Arrays used instead of string concatenation
|
||||
|
||||
### File Operations
|
||||
- [ ] All paths validated against directory traversal
|
||||
- [ ] Temp files created with mktemp
|
||||
- [ ] File permissions set restrictively (600/700)
|
||||
- [ ] Cleanup handlers registered (trap EXIT)
|
||||
|
||||
### Secrets
|
||||
- [ ] No hardcoded passwords/keys/tokens
|
||||
- [ ] Secrets read from secure storage
|
||||
- [ ] Secrets never logged or printed
|
||||
- [ ] Secrets cleared from memory when done
|
||||
|
||||
### Privileges
|
||||
- [ ] Runs with minimum required privileges
|
||||
- [ ] Root execution rejected unless necessary
|
||||
- [ ] Privilege drops implemented where needed
|
||||
- [ ] Sudo scope minimized
|
||||
|
||||
### Error Handling
|
||||
- [ ] set -euo pipefail enabled
|
||||
- [ ] All errors logged to stderr
|
||||
- [ ] Sensitive data not exposed in errors
|
||||
- [ ] Exit codes meaningful
|
||||
|
||||
## Automated Security Scanning
|
||||
|
||||
### ShellCheck Integration
|
||||
|
||||
```bash
|
||||
# .github/workflows/security.yml
|
||||
name: Security Scan
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
shellcheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: ShellCheck
|
||||
run: |
|
||||
# Fail on security issues
|
||||
find . -name "*.sh" -exec shellcheck \
|
||||
--severity=error \
|
||||
--enable=all \
|
||||
{} +
|
||||
```
|
||||
|
||||
### Custom Security Linting
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# security-lint.sh - Check scripts for security issues
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
lint_script() {
|
||||
local script="$1"
|
||||
local issues=0
|
||||
|
||||
echo "Checking: $script"
|
||||
|
||||
# Check for eval
|
||||
if grep -n "eval" "$script"; then
|
||||
echo " ❌ Found eval (command injection risk)"
|
||||
((issues++))
|
||||
fi
|
||||
|
||||
# Check for hardcoded secrets
|
||||
if grep -nE "(password|secret|token|key)\s*=\s*['\"][^'\"]+['\"]" "$script"; then
|
||||
echo " ❌ Found hardcoded secrets"
|
||||
((issues++))
|
||||
fi
|
||||
|
||||
# Check for predictable temp files
|
||||
if grep -n "/tmp/[a-zA-Z0-9_-]*\\.tmp" "$script"; then
|
||||
echo " ❌ Found predictable temp file"
|
||||
((issues++))
|
||||
fi
|
||||
|
||||
# Check for unquoted variables
|
||||
if grep -nE '\$[A-Z_]+[^"]' "$script"; then
|
||||
echo " ⚠️ Found unquoted variables"
|
||||
((issues++))
|
||||
fi
|
||||
|
||||
if ((issues == 0)); then
|
||||
echo " ✓ No security issues found"
|
||||
fi
|
||||
|
||||
return "$issues"
|
||||
}
|
||||
|
||||
# Scan all scripts
|
||||
total_issues=0
|
||||
while IFS= read -r -d '' script; do
|
||||
lint_script "$script" || ((total_issues++))
|
||||
done < <(find . -name "*.sh" -type f -print0)
|
||||
|
||||
if ((total_issues > 0)); then
|
||||
echo "❌ Found security issues in $total_issues scripts"
|
||||
exit 1
|
||||
else
|
||||
echo "✓ All scripts passed security checks"
|
||||
fi
|
||||
```
|
||||
|
||||
## Real-World Secure Script Template
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Secure Script Template (2025)
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
IFS=$'\n\t'
|
||||
|
||||
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
|
||||
|
||||
# Security: Reject root execution
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
echo "Error: Do not run as root" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Security: Clean environment
|
||||
export PATH="/usr/local/bin:/usr/bin:/bin"
|
||||
unset CDPATH GLOBIGNORE
|
||||
|
||||
# Security: Secure temp file
|
||||
readonly TEMP_FILE=$(mktemp)
|
||||
trap 'rm -f "$TEMP_FILE"; exit' EXIT INT TERM
|
||||
chmod 600 "$TEMP_FILE"
|
||||
|
||||
# Validate input
|
||||
validate_input() {
|
||||
local input="$1"
|
||||
|
||||
if [[ -z "$input" ]]; then
|
||||
echo "Error: Input required" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ ! "$input" =~ ^[a-zA-Z0-9_/-]+$ ]]; then
|
||||
echo "Error: Invalid characters in input" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ ${#input} -gt 255 ]]; then
|
||||
echo "Error: Input too long" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Sanitize file path
|
||||
sanitize_path() {
|
||||
local path="$1"
|
||||
path="${path//..\/}"
|
||||
path="${path#/}"
|
||||
echo "$path"
|
||||
}
|
||||
|
||||
# Main function
|
||||
main() {
|
||||
local user_input="${1:-}"
|
||||
|
||||
# Validate
|
||||
if ! validate_input "$user_input"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Sanitize
|
||||
local safe_path
|
||||
safe_path=$(sanitize_path "$user_input")
|
||||
|
||||
# Process safely
|
||||
echo "Processing: $safe_path"
|
||||
# ... your logic here ...
|
||||
}
|
||||
|
||||
main "$@"
|
||||
```
|
||||
|
||||
## Compliance Standards (2025)
|
||||
|
||||
### CIS Benchmarks
|
||||
- Use ShellCheck for automated compliance
|
||||
- Implement input validation on all user data
|
||||
- Secure temporary file handling
|
||||
- Least privilege execution
|
||||
|
||||
### NIST Guidelines
|
||||
- Strong input validation (NIST SP 800-53)
|
||||
- Secure coding practices
|
||||
- Logging and monitoring
|
||||
- Access control enforcement
|
||||
|
||||
### OWASP Top 10
|
||||
- A03: Injection - Prevent command injection
|
||||
- A01: Broken Access Control - Path validation
|
||||
- A02: Cryptographic Failures - Secure secrets
|
||||
|
||||
## Resources
|
||||
|
||||
- [CIS Docker Benchmark](https://www.cisecurity.org/benchmark/docker)
|
||||
- [OWASP Command Injection](https://owasp.org/www-community/attacks/Command_Injection)
|
||||
- [ShellCheck Security Rules](https://www.shellcheck.net/wiki/)
|
||||
- [NIST SP 800-53](https://csrc.nist.gov/publications/detail/sp/800-53/rev-5/final)
|
||||
|
||||
---
|
||||
|
||||
**Security-first development is non-negotiable in 2025. Every script must pass all security checks before deployment.**
|
||||
Reference in New Issue
Block a user