📦 deps(thirdparty): update snapshots
This commit is contained in:
@@ -0,0 +1,533 @@
|
||||
# Security Best Practices
|
||||
|
||||
Comprehensive security guidance for Claude Code hooks.
|
||||
|
||||
## Input Validation
|
||||
|
||||
### Validate All Input
|
||||
|
||||
Always validate and sanitize hook input before use:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
|
||||
|
||||
# Validate input exists
|
||||
if [[ -z "$FILE_PATH" ]]; then
|
||||
echo "Error: file_path missing" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate format
|
||||
if [[ ! "$FILE_PATH" =~ ^[a-zA-Z0-9_./-]+$ ]]; then
|
||||
echo "Error: invalid characters in file path" >&2
|
||||
exit 2
|
||||
fi
|
||||
```
|
||||
|
||||
### Check for Path Traversal
|
||||
|
||||
Block directory traversal attacks:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
|
||||
|
||||
# Block path traversal
|
||||
if echo "$FILE_PATH" | grep -qE '\.\./'; then
|
||||
cat << EOF >&2
|
||||
Path traversal detected: $FILE_PATH
|
||||
Paths containing '..' are not allowed.
|
||||
EOF
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Block absolute paths outside project
|
||||
if [[ "$FILE_PATH" == /* ]] && [[ ! "$FILE_PATH" == "$CLAUDE_PROJECT_DIR"* ]]; then
|
||||
echo "Access outside project directory blocked: $FILE_PATH" >&2
|
||||
exit 2
|
||||
fi
|
||||
```
|
||||
|
||||
### Block Sensitive System Paths
|
||||
|
||||
Prevent access to system files:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
|
||||
|
||||
# Blocked system paths
|
||||
BLOCKED_PATHS=(
|
||||
'^/etc/'
|
||||
'^/root/'
|
||||
'^/home/[^/]+/\.ssh/'
|
||||
'^/var/log/'
|
||||
'^/sys/'
|
||||
'^/proc/'
|
||||
'^/boot/'
|
||||
'^/usr/bin/'
|
||||
'^/usr/sbin/'
|
||||
)
|
||||
|
||||
for pattern in "${BLOCKED_PATHS[@]}"; do
|
||||
if echo "$FILE_PATH" | grep -qE "$pattern"; then
|
||||
cat << EOF >&2
|
||||
Access to sensitive system path blocked: $FILE_PATH
|
||||
This path is restricted for security reasons.
|
||||
EOF
|
||||
exit 2
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
### Detect Sensitive Files
|
||||
|
||||
Warn or block access to sensitive project files:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
|
||||
|
||||
# Sensitive file patterns
|
||||
SENSITIVE_PATTERNS=(
|
||||
'\.env$'
|
||||
'\.env\.'
|
||||
'id_rsa'
|
||||
'id_ed25519'
|
||||
'\.pem$'
|
||||
'\.key$'
|
||||
'\.p12$'
|
||||
'credentials'
|
||||
'password'
|
||||
'token'
|
||||
'secret'
|
||||
'\.git/config$'
|
||||
'\.npmrc$'
|
||||
'\.pypirc$'
|
||||
)
|
||||
|
||||
for pattern in "${SENSITIVE_PATTERNS[@]}"; do
|
||||
if echo "$FILE_PATH" | grep -qiE "$pattern"; then
|
||||
cat << EOF >&2
|
||||
Warning: Accessing sensitive file: $FILE_PATH
|
||||
This file may contain sensitive information.
|
||||
EOF
|
||||
# Could exit 2 to block, or continue with warning
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
## Command Injection Prevention
|
||||
|
||||
### Always Quote Variables
|
||||
|
||||
```bash
|
||||
# WRONG - vulnerable to injection
|
||||
rm $FILE_PATH
|
||||
cd $DIRECTORY
|
||||
echo $CONTENT
|
||||
|
||||
# CORRECT - properly quoted
|
||||
rm "$FILE_PATH"
|
||||
cd "$DIRECTORY"
|
||||
echo "$CONTENT"
|
||||
```
|
||||
|
||||
### Avoid eval
|
||||
|
||||
```bash
|
||||
# WRONG - dangerous
|
||||
eval "$USER_COMMAND"
|
||||
|
||||
# CORRECT - use specific commands
|
||||
if [[ "$USER_COMMAND" == "format" ]]; then
|
||||
black "$FILE_PATH"
|
||||
fi
|
||||
```
|
||||
|
||||
### Validate Command Patterns
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
|
||||
|
||||
# Block dangerous command patterns
|
||||
DANGEROUS_PATTERNS=(
|
||||
'\brm\s+-rf\s+/' # rm -rf /
|
||||
'\brm\s+--no-preserve-root' # rm --no-preserve-root
|
||||
'\bmkfs\b' # filesystem format
|
||||
'\bdd\s+if=' # disk destruction
|
||||
'\bformat\s+[cC]:' # Windows format
|
||||
'>\s*/dev/sd[a-z]' # overwrite disk
|
||||
':()\{\s*:\|\:&\s*\};:' # Fork bomb
|
||||
'\bchmod\s+777\s+/' # Dangerous permissions
|
||||
'\bchown\s+.*\s+/' # System ownership change
|
||||
'\bcurl\s+.*\|\s*bash' # Pipe to bash
|
||||
'\bwget\s+.*\|\s*bash' # Pipe to bash
|
||||
'\bsudo\s+rm' # Sudo rm
|
||||
'\bgit\s+push\s+--force\s+origin\s+main' # Force push main
|
||||
)
|
||||
|
||||
for pattern in "${DANGEROUS_PATTERNS[@]}"; do
|
||||
if echo "$COMMAND" | grep -qE "$pattern"; then
|
||||
cat << EOF >&2
|
||||
Dangerous command blocked: $COMMAND
|
||||
Pattern matched: $pattern
|
||||
EOF
|
||||
exit 2
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
## Path Security
|
||||
|
||||
### Use Absolute Paths
|
||||
|
||||
Always construct paths from known roots:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# Use CLAUDE_PROJECT_DIR for project paths
|
||||
SCRIPT_PATH="$CLAUDE_PROJECT_DIR/.claude/hooks/helper.sh"
|
||||
|
||||
# Use CLAUDE_PLUGIN_ROOT for plugin paths
|
||||
PLUGIN_SCRIPT="${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh"
|
||||
|
||||
# Never rely on relative paths
|
||||
# BAD: ./scripts/validate.sh
|
||||
# GOOD: "$CLAUDE_PROJECT_DIR/.claude/scripts/validate.sh"
|
||||
```
|
||||
|
||||
### Validate Script Existence
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
SCRIPT_PATH="$CLAUDE_PROJECT_DIR/.claude/hooks/helper.sh"
|
||||
|
||||
# Check exists
|
||||
if [[ ! -f "$SCRIPT_PATH" ]]; then
|
||||
echo "Error: script not found: $SCRIPT_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check executable
|
||||
if [[ ! -x "$SCRIPT_PATH" ]]; then
|
||||
echo "Error: script not executable: $SCRIPT_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Execute safely
|
||||
"$SCRIPT_PATH" "$@"
|
||||
```
|
||||
|
||||
### Resolve Symlinks
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path')
|
||||
|
||||
# Resolve symlinks to check actual destination
|
||||
REAL_PATH=$(realpath "$FILE_PATH" 2>/dev/null || echo "$FILE_PATH")
|
||||
|
||||
# Check the real path is within project
|
||||
if [[ ! "$REAL_PATH" == "$CLAUDE_PROJECT_DIR"* ]]; then
|
||||
echo "Symlink points outside project: $FILE_PATH -> $REAL_PATH" >&2
|
||||
exit 2
|
||||
fi
|
||||
```
|
||||
|
||||
## Sensitive Data Protection
|
||||
|
||||
### Never Log Sensitive Data
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
INPUT=$(cat)
|
||||
|
||||
# WRONG - logs everything including secrets
|
||||
echo "Input: $INPUT" >> /tmp/debug.log
|
||||
|
||||
# CORRECT - log only safe fields
|
||||
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
|
||||
echo "Tool: $TOOL_NAME" >> /tmp/debug.log
|
||||
|
||||
# CORRECT - filter sensitive fields before logging
|
||||
echo "$INPUT" | jq 'del(.tool_input.password, .tool_input.api_key, .tool_input.token)' >> /tmp/debug.log
|
||||
```
|
||||
|
||||
### Sanitize Output
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
INPUT=$(cat)
|
||||
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // empty')
|
||||
|
||||
# Check for secrets in content
|
||||
if echo "$CONTENT" | grep -qiE '(password|api_key|secret|token)\s*[=:]\s*\S+'; then
|
||||
echo "Warning: Potential secret detected in content" >&2
|
||||
# Could block or just warn
|
||||
fi
|
||||
```
|
||||
|
||||
### Protect Environment Variables
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# Don't expose sensitive env vars
|
||||
|
||||
# WRONG
|
||||
echo "API_KEY=$API_KEY"
|
||||
env | grep -i secret
|
||||
|
||||
# CORRECT - never print secrets
|
||||
echo "API key configured: $([ -n "$API_KEY" ] && echo "yes" || echo "no")"
|
||||
```
|
||||
|
||||
## Timeout Protection
|
||||
|
||||
### Set Appropriate Timeouts
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [{
|
||||
"matcher": "Bash",
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "./.claude/hooks/validate.sh",
|
||||
"timeout": 5
|
||||
}]
|
||||
}],
|
||||
"PostToolUse": [{
|
||||
"matcher": "Write|Edit",
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "./.claude/hooks/format.sh",
|
||||
"timeout": 30
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Guidelines**:
|
||||
- Quick validation: 3-5 seconds
|
||||
- Formatting: 10-30 seconds
|
||||
- Network operations: 30-60 seconds
|
||||
- Default: 60 seconds for command, 30 seconds for prompt
|
||||
|
||||
### Handle Timeouts Gracefully
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Set internal timeout for network operations
|
||||
timeout 10 curl -s https://api.example.com/validate || {
|
||||
echo "API validation skipped (timeout)" >&2
|
||||
exit 0 # Don't block on timeout
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Use Strict Mode
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail # Exit on error, undefined vars, pipe failures
|
||||
|
||||
# Also consider:
|
||||
set -E # Inherit ERR trap in functions
|
||||
trap 'echo "Error on line $LINENO" >&2' ERR
|
||||
```
|
||||
|
||||
### Validate Dependencies
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Check required tools exist
|
||||
for cmd in jq git curl; do
|
||||
if ! command -v "$cmd" &>/dev/null; then
|
||||
echo "Error: $cmd not installed" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
### Handle JSON Parsing Errors
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Read input with error handling
|
||||
INPUT=$(cat) || {
|
||||
echo "Error: failed to read stdin" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Parse with validation
|
||||
if ! echo "$INPUT" | jq empty 2>/dev/null; then
|
||||
echo "Error: invalid JSON input" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
|
||||
if [[ -z "$TOOL_NAME" ]]; then
|
||||
echo "Error: tool_name missing" >&2
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
## Permission Control
|
||||
|
||||
### PreToolUse Permission Decisions
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
|
||||
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
|
||||
|
||||
# Auto-approve reads of non-sensitive files
|
||||
if [[ "$TOOL_NAME" == "Read" ]] && [[ ! "$FILE_PATH" =~ \.(env|key|pem)$ ]]; then
|
||||
cat << EOF
|
||||
{
|
||||
"continue": true,
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PreToolUse",
|
||||
"permissionDecision": "allow"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Ask for writes to core files
|
||||
if [[ "$FILE_PATH" =~ src/core/ ]]; then
|
||||
cat << EOF
|
||||
{
|
||||
"continue": true,
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PreToolUse",
|
||||
"permissionDecision": "ask",
|
||||
"permissionDecisionReason": "Write to core module requires confirmation"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Default: allow
|
||||
exit 0
|
||||
```
|
||||
|
||||
### PermissionRequest Hook
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
|
||||
|
||||
# Auto-deny certain operations
|
||||
if [[ "$TOOL_NAME" =~ (delete|destroy|remove) ]]; then
|
||||
cat << EOF
|
||||
{
|
||||
"hookSpecificOutput": {
|
||||
"permissionDecision": "deny",
|
||||
"permissionDecisionReason": "Destructive operations require manual approval"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
exit 0
|
||||
fi
|
||||
```
|
||||
|
||||
## Audit Trail
|
||||
|
||||
### Log All Operations
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
AUDIT_FILE="$CLAUDE_PROJECT_DIR/.claude/audit.log"
|
||||
|
||||
INPUT=$(cat)
|
||||
TIMESTAMP=$(date -Iseconds)
|
||||
HOOK_EVENT=$(echo "$INPUT" | jq -r '.hook_event_name')
|
||||
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // "N/A"')
|
||||
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // "N/A"')
|
||||
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id')
|
||||
|
||||
# Create audit entry (filter sensitive data)
|
||||
AUDIT_ENTRY=$(jq -n \
|
||||
--arg ts "$TIMESTAMP" \
|
||||
--arg event "$HOOK_EVENT" \
|
||||
--arg tool "$TOOL_NAME" \
|
||||
--arg file "$FILE_PATH" \
|
||||
--arg session "$SESSION_ID" \
|
||||
--arg user "$USER" \
|
||||
'{
|
||||
timestamp: $ts,
|
||||
event: $event,
|
||||
tool: $tool,
|
||||
file: $file,
|
||||
session: $session,
|
||||
user: $user
|
||||
}')
|
||||
|
||||
echo "$AUDIT_ENTRY" >> "$AUDIT_FILE"
|
||||
|
||||
# Rotate: keep only last 10000 entries
|
||||
tail -n 10000 "$AUDIT_FILE" > "$AUDIT_FILE.tmp" && mv "$AUDIT_FILE.tmp" "$AUDIT_FILE"
|
||||
|
||||
exit 0
|
||||
```
|
||||
|
||||
## Security Checklist
|
||||
|
||||
### Before Deploying Hooks
|
||||
|
||||
- [ ] All input validated and sanitized
|
||||
- [ ] Path traversal attacks blocked
|
||||
- [ ] Sensitive system paths protected
|
||||
- [ ] All shell variables quoted
|
||||
- [ ] No eval or command injection vectors
|
||||
- [ ] Sensitive data not logged
|
||||
- [ ] Appropriate timeouts set
|
||||
- [ ] Dependencies validated
|
||||
- [ ] Error handling robust
|
||||
- [ ] Audit trail enabled
|
||||
|
||||
### Regular Security Review
|
||||
|
||||
- [ ] Review hook scripts for vulnerabilities
|
||||
- [ ] Check for hardcoded secrets
|
||||
- [ ] Verify timeout values are appropriate
|
||||
- [ ] Audit logged data for sensitive info leaks
|
||||
- [ ] Update blocked patterns for new threats
|
||||
- [ ] Test hooks with malicious input
|
||||
Reference in New Issue
Block a user