mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
clean: squash history (binaries stripped via filter-repo)
This commit is contained in:
@@ -0,0 +1,349 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Package security provides security testing and validation
|
||||
package security
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestTLSConfiguration ensures proper TLS configuration
|
||||
func TestTLSConfiguration(t *testing.T) {
|
||||
config := &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
CipherSuites: []uint16{
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
},
|
||||
PreferServerCipherSuites: true,
|
||||
InsecureSkipVerify: false,
|
||||
}
|
||||
|
||||
// Verify minimum TLS version
|
||||
require.GreaterOrEqual(t, config.MinVersion, uint16(tls.VersionTLS12), "TLS version must be 1.2 or higher")
|
||||
|
||||
// Verify InsecureSkipVerify is false
|
||||
require.False(t, config.InsecureSkipVerify, "InsecureSkipVerify must be false in production")
|
||||
|
||||
// Verify strong cipher suites
|
||||
require.NotEmpty(t, config.CipherSuites, "Cipher suites must be explicitly configured")
|
||||
}
|
||||
|
||||
// TestIntegerOverflowProtection tests for integer overflow vulnerabilities
|
||||
func TestIntegerOverflowProtection(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fn func() error
|
||||
}{
|
||||
{
|
||||
name: "uint32 to int conversion",
|
||||
fn: func() error {
|
||||
var u32 uint32 = math.MaxUint32
|
||||
if int(u32) < 0 {
|
||||
return fmt.Errorf("integer overflow detected")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "int64 multiplication overflow",
|
||||
fn: func() error {
|
||||
a := int64(math.MaxInt64 / 2)
|
||||
b := int64(3)
|
||||
// Check for overflow before multiplication
|
||||
if a > 0 && b > 0 && a > math.MaxInt64/b {
|
||||
return fmt.Errorf("multiplication would overflow")
|
||||
}
|
||||
_ = a * b
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "safe string to int conversion",
|
||||
fn: func() error {
|
||||
input := "9223372036854775807" // MaxInt64
|
||||
val, err := strconv.ParseInt(input, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if val < 0 {
|
||||
return fmt.Errorf("unexpected negative value")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.fn()
|
||||
// Some tests expect errors for overflow detection
|
||||
if strings.Contains(tt.name, "overflow") && err != nil {
|
||||
// Expected error for overflow detection
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestInputValidation ensures proper input validation
|
||||
func TestInputValidation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
validate func(string) error
|
||||
}{
|
||||
{
|
||||
name: "numeric input validation",
|
||||
input: "12345",
|
||||
validate: func(s string) error {
|
||||
val, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if val < 0 || val > 65535 {
|
||||
return fmt.Errorf("value out of range")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "path traversal prevention",
|
||||
input: "../../../etc/passwd",
|
||||
validate: func(s string) error {
|
||||
if strings.Contains(s, "..") {
|
||||
return fmt.Errorf("path traversal detected")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SQL injection prevention",
|
||||
input: "'; DROP TABLE users; --",
|
||||
validate: func(s string) error {
|
||||
dangerous := []string{"DROP", "DELETE", "INSERT", "UPDATE", ";", "--"}
|
||||
upper := strings.ToUpper(s)
|
||||
for _, d := range dangerous {
|
||||
if strings.Contains(upper, d) {
|
||||
return fmt.Errorf("potentially dangerous SQL detected")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.validate(tt.input)
|
||||
// Path traversal and SQL injection tests should fail
|
||||
if strings.Contains(tt.name, "traversal") || strings.Contains(tt.name, "injection") {
|
||||
require.Error(t, err, "Should detect and prevent %s", tt.name)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestCryptographicRandomness ensures proper use of crypto/rand
|
||||
func TestCryptographicRandomness(t *testing.T) {
|
||||
// Test generation of cryptographically secure random bytes
|
||||
sizes := []int{16, 32, 64, 128}
|
||||
|
||||
for _, size := range sizes {
|
||||
t.Run(fmt.Sprintf("size_%d", size), func(t *testing.T) {
|
||||
buf := make([]byte, size)
|
||||
n, err := rand.Read(buf)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, size, n)
|
||||
|
||||
// Verify randomness (basic check - no zeros)
|
||||
allZeros := true
|
||||
for _, b := range buf {
|
||||
if b != 0 {
|
||||
allZeros = false
|
||||
break
|
||||
}
|
||||
}
|
||||
require.False(t, allZeros, "Random bytes should not be all zeros")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestHTTPClientTimeout ensures HTTP clients have proper timeouts
|
||||
func TestHTTPClientTimeout(t *testing.T) {
|
||||
client := &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ResponseHeaderTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
MaxIdleConns: 100,
|
||||
MaxIdleConnsPerHost: 10,
|
||||
},
|
||||
}
|
||||
|
||||
// Verify timeout is set
|
||||
require.NotEqual(t, 0, client.Timeout, "HTTP client must have a timeout")
|
||||
require.LessOrEqual(t, client.Timeout, 60*time.Second, "HTTP client timeout should be reasonable")
|
||||
|
||||
// Verify transport timeouts
|
||||
transport := client.Transport.(*http.Transport)
|
||||
require.NotEqual(t, 0, transport.TLSHandshakeTimeout, "TLS handshake timeout must be set")
|
||||
require.NotEqual(t, 0, transport.ResponseHeaderTimeout, "Response header timeout must be set")
|
||||
}
|
||||
|
||||
// TestRateLimiting provides a basic rate limiting test
|
||||
func TestRateLimiting(t *testing.T) {
|
||||
type rateLimiter struct {
|
||||
requests map[string][]time.Time
|
||||
limit int
|
||||
window time.Duration
|
||||
}
|
||||
|
||||
rl := &rateLimiter{
|
||||
requests: make(map[string][]time.Time),
|
||||
limit: 10,
|
||||
window: time.Minute,
|
||||
}
|
||||
|
||||
checkLimit := func(clientID string) bool {
|
||||
now := time.Now()
|
||||
requests := rl.requests[clientID]
|
||||
|
||||
// Remove old requests outside the window
|
||||
validRequests := []time.Time{}
|
||||
for _, reqTime := range requests {
|
||||
if now.Sub(reqTime) <= rl.window {
|
||||
validRequests = append(validRequests, reqTime)
|
||||
}
|
||||
}
|
||||
|
||||
if len(validRequests) >= rl.limit {
|
||||
return false // Rate limit exceeded
|
||||
}
|
||||
|
||||
validRequests = append(validRequests, now)
|
||||
rl.requests[clientID] = validRequests
|
||||
return true
|
||||
}
|
||||
|
||||
clientID := "test-client"
|
||||
|
||||
// Should allow first requests
|
||||
for i := 0; i < 10; i++ {
|
||||
allowed := checkLimit(clientID)
|
||||
require.True(t, allowed, "Request %d should be allowed", i+1)
|
||||
}
|
||||
|
||||
// Should block after limit
|
||||
allowed := checkLimit(clientID)
|
||||
require.False(t, allowed, "Request should be blocked after rate limit")
|
||||
}
|
||||
|
||||
// TestAccessControl verifies basic access control patterns
|
||||
func TestAccessControl(t *testing.T) {
|
||||
type permission string
|
||||
const (
|
||||
permRead permission = "read"
|
||||
permWrite permission = "write"
|
||||
permAdmin permission = "admin"
|
||||
)
|
||||
|
||||
type role struct {
|
||||
name string
|
||||
permissions []permission
|
||||
}
|
||||
|
||||
roles := map[string]role{
|
||||
"viewer": {
|
||||
name: "viewer",
|
||||
permissions: []permission{permRead},
|
||||
},
|
||||
"editor": {
|
||||
name: "editor",
|
||||
permissions: []permission{permRead, permWrite},
|
||||
},
|
||||
"admin": {
|
||||
name: "admin",
|
||||
permissions: []permission{permRead, permWrite, permAdmin},
|
||||
},
|
||||
}
|
||||
|
||||
hasPermission := func(roleName string, perm permission) bool {
|
||||
role, exists := roles[roleName]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
for _, p := range role.permissions {
|
||||
if p == perm {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Test permission checks
|
||||
require.True(t, hasPermission("viewer", permRead))
|
||||
require.False(t, hasPermission("viewer", permWrite))
|
||||
require.False(t, hasPermission("viewer", permAdmin))
|
||||
|
||||
require.True(t, hasPermission("editor", permRead))
|
||||
require.True(t, hasPermission("editor", permWrite))
|
||||
require.False(t, hasPermission("editor", permAdmin))
|
||||
|
||||
require.True(t, hasPermission("admin", permRead))
|
||||
require.True(t, hasPermission("admin", permWrite))
|
||||
require.True(t, hasPermission("admin", permAdmin))
|
||||
|
||||
// Test non-existent role
|
||||
require.False(t, hasPermission("nonexistent", permRead))
|
||||
}
|
||||
|
||||
// TestSecureStringComparison tests constant-time string comparison
|
||||
func TestSecureStringComparison(t *testing.T) {
|
||||
// For security-sensitive comparisons (like tokens, passwords),
|
||||
// use constant-time comparison to prevent timing attacks
|
||||
constantTimeCompare := func(a, b string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
|
||||
var result byte
|
||||
for i := 0; i < len(a); i++ {
|
||||
result |= a[i] ^ b[i]
|
||||
}
|
||||
return result == 0
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
a, b string
|
||||
expected bool
|
||||
}{
|
||||
{"password123", "password123", true},
|
||||
{"password123", "password124", false},
|
||||
{"short", "longer", false},
|
||||
{"", "", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := constantTimeCompare(tt.a, tt.b)
|
||||
require.Equal(t, tt.expected, result, "Comparing %q and %q", tt.a, tt.b)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Package security provides security validation and protection utilities
|
||||
package security
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// InputValidator provides methods for validating and sanitizing user input
|
||||
type InputValidator struct {
|
||||
// MaxStringLength is the maximum allowed string length
|
||||
MaxStringLength int
|
||||
// AllowedPathPrefixes are the allowed path prefixes for file operations
|
||||
AllowedPathPrefixes []string
|
||||
}
|
||||
|
||||
// NewInputValidator creates a new input validator with default settings
|
||||
func NewInputValidator() *InputValidator {
|
||||
return &InputValidator{
|
||||
MaxStringLength: 10000,
|
||||
AllowedPathPrefixes: []string{"/tmp", "/var/tmp"},
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateStringLength checks if a string length is within acceptable bounds
|
||||
func (v *InputValidator) ValidateStringLength(s string) error {
|
||||
if len(s) > v.MaxStringLength {
|
||||
return fmt.Errorf("string length %d exceeds maximum allowed %d", len(s), v.MaxStringLength)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateFilePath validates a file path for security issues
|
||||
func (v *InputValidator) ValidateFilePath(path string) error {
|
||||
// Clean the path to resolve . and .. elements
|
||||
cleanPath := filepath.Clean(path)
|
||||
|
||||
// Check for path traversal attempts
|
||||
if strings.Contains(path, "..") {
|
||||
return fmt.Errorf("path traversal detected in path: %s", path)
|
||||
}
|
||||
|
||||
// Ensure the path is absolute
|
||||
if !filepath.IsAbs(cleanPath) {
|
||||
return fmt.Errorf("path must be absolute: %s", cleanPath)
|
||||
}
|
||||
|
||||
// Check if path is within allowed prefixes
|
||||
allowed := false
|
||||
for _, prefix := range v.AllowedPathPrefixes {
|
||||
if strings.HasPrefix(cleanPath, prefix) {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !allowed && len(v.AllowedPathPrefixes) > 0 {
|
||||
return fmt.Errorf("path %s is not within allowed prefixes", cleanPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateIPAddress validates an IP address or hostname
|
||||
func (v *InputValidator) ValidateIPAddress(addr string) error {
|
||||
// Check if it's a hostname:port combination and extract the host
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err == nil {
|
||||
addr = host
|
||||
}
|
||||
|
||||
// First check if it looks like an IPv4 address and validate properly
|
||||
if strings.Count(addr, ".") == 3 && !strings.Contains(addr, ":") {
|
||||
parts := strings.Split(addr, ".")
|
||||
allNumeric := true
|
||||
for _, part := range parts {
|
||||
if part == "" {
|
||||
allNumeric = false
|
||||
break
|
||||
}
|
||||
for _, ch := range part {
|
||||
if ch < '0' || ch > '9' {
|
||||
allNumeric = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allNumeric {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Only validate as IPv4 if all parts are numeric
|
||||
if allNumeric {
|
||||
for _, part := range parts {
|
||||
num := 0
|
||||
if _, err := fmt.Sscanf(part, "%d", &num); err != nil || num < 0 || num > 255 {
|
||||
return fmt.Errorf("invalid IPv4 address: %s", addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it's a valid IP address
|
||||
ip := net.ParseIP(addr)
|
||||
if ip != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Basic hostname validation
|
||||
if len(addr) > 253 {
|
||||
return fmt.Errorf("hostname too long: %d characters", len(addr))
|
||||
}
|
||||
|
||||
// Check for valid hostname pattern
|
||||
hostnameRegex := regexp.MustCompile(`^[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])?)*$`)
|
||||
if !hostnameRegex.MatchString(addr) {
|
||||
return fmt.Errorf("invalid hostname or IP address: %s", addr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidatePort validates a network port number
|
||||
func (v *InputValidator) ValidatePort(port int) error {
|
||||
if port < 1 || port > 65535 {
|
||||
return fmt.Errorf("invalid port number: %d (must be between 1 and 65535)", port)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SafeIntConversion safely converts between integer types checking for overflow
|
||||
func SafeIntConversion(value int64, bitSize int) (int64, error) {
|
||||
switch bitSize {
|
||||
case 8:
|
||||
if value < math.MinInt8 || value > math.MaxInt8 {
|
||||
return 0, fmt.Errorf("value %d overflows int8", value)
|
||||
}
|
||||
case 16:
|
||||
if value < math.MinInt16 || value > math.MaxInt16 {
|
||||
return 0, fmt.Errorf("value %d overflows int16", value)
|
||||
}
|
||||
case 32:
|
||||
if value < math.MinInt32 || value > math.MaxInt32 {
|
||||
return 0, fmt.Errorf("value %d overflows int32", value)
|
||||
}
|
||||
case 64:
|
||||
// Already int64, no overflow possible
|
||||
default:
|
||||
return 0, fmt.Errorf("unsupported bit size: %d", bitSize)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// SafeUintConversion safely converts to unsigned integer checking for overflow
|
||||
func SafeUintConversion(value int64, bitSize int) (uint64, error) {
|
||||
if value < 0 {
|
||||
return 0, fmt.Errorf("cannot convert negative value %d to unsigned", value)
|
||||
}
|
||||
|
||||
uvalue := uint64(value)
|
||||
switch bitSize {
|
||||
case 8:
|
||||
if uvalue > math.MaxUint8 {
|
||||
return 0, fmt.Errorf("value %d overflows uint8", value)
|
||||
}
|
||||
case 16:
|
||||
if uvalue > math.MaxUint16 {
|
||||
return 0, fmt.Errorf("value %d overflows uint16", value)
|
||||
}
|
||||
case 32:
|
||||
if uvalue > math.MaxUint32 {
|
||||
return 0, fmt.Errorf("value %d overflows uint32", value)
|
||||
}
|
||||
case 64:
|
||||
// Already uint64, no overflow possible from positive int64
|
||||
default:
|
||||
return 0, fmt.Errorf("unsupported bit size: %d", bitSize)
|
||||
}
|
||||
return uvalue, nil
|
||||
}
|
||||
|
||||
// ConstantTimeCompare performs constant-time comparison of two strings
|
||||
// Returns true if strings are equal, false otherwise
|
||||
func ConstantTimeCompare(a, b string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
|
||||
aBytes := []byte(a)
|
||||
bBytes := []byte(b)
|
||||
return subtle.ConstantTimeCompare(aBytes, bBytes) == 1
|
||||
}
|
||||
|
||||
// SanitizeSQL performs basic SQL injection prevention
|
||||
// This is a basic implementation - use prepared statements for production
|
||||
func SanitizeSQL(input string) string {
|
||||
// Remove or escape potentially dangerous characters
|
||||
replacer := strings.NewReplacer(
|
||||
";", "",
|
||||
"--", "",
|
||||
"/*", "",
|
||||
"*/", "",
|
||||
"xp_", "",
|
||||
"sp_", "",
|
||||
"'", "''",
|
||||
"\"", "\"\"",
|
||||
)
|
||||
return replacer.Replace(input)
|
||||
}
|
||||
|
||||
// ValidateAlphanumeric checks if a string contains only alphanumeric characters
|
||||
func ValidateAlphanumeric(s string) error {
|
||||
alphanumeric := regexp.MustCompile(`^[a-zA-Z0-9]+$`)
|
||||
if !alphanumeric.MatchString(s) {
|
||||
return fmt.Errorf("string contains non-alphanumeric characters")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateEmail performs basic email validation
|
||||
func ValidateEmail(email string) error {
|
||||
// Basic email regex pattern
|
||||
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
|
||||
if !emailRegex.MatchString(email) {
|
||||
return fmt.Errorf("invalid email format")
|
||||
}
|
||||
if len(email) > 254 { // RFC 5321
|
||||
return fmt.Errorf("email address too long")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateURL performs basic URL validation
|
||||
func ValidateURL(url string) error {
|
||||
// Check for common URL injection patterns
|
||||
dangerous := []string{
|
||||
"javascript:",
|
||||
"data:",
|
||||
"vbscript:",
|
||||
"file:",
|
||||
"<script",
|
||||
"onclick",
|
||||
"onerror",
|
||||
}
|
||||
|
||||
lowerURL := strings.ToLower(url)
|
||||
for _, pattern := range dangerous {
|
||||
if strings.Contains(lowerURL, pattern) {
|
||||
return fmt.Errorf("potentially dangerous URL pattern detected: %s", pattern)
|
||||
}
|
||||
}
|
||||
|
||||
// Basic URL format check
|
||||
if !strings.HasPrefix(lowerURL, "http://") && !strings.HasPrefix(lowerURL, "https://") {
|
||||
return fmt.Errorf("URL must start with http:// or https://")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RateLimiter provides basic rate limiting functionality
|
||||
type RateLimiter struct {
|
||||
requests map[string][]int64
|
||||
limit int
|
||||
window int64 // in seconds
|
||||
}
|
||||
|
||||
// NewRateLimiter creates a new rate limiter
|
||||
func NewRateLimiter(limit int, windowSeconds int64) *RateLimiter {
|
||||
return &RateLimiter{
|
||||
requests: make(map[string][]int64),
|
||||
limit: limit,
|
||||
window: windowSeconds,
|
||||
}
|
||||
}
|
||||
|
||||
// Allow checks if a request should be allowed based on rate limits
|
||||
func (r *RateLimiter) Allow(clientID string, now int64) bool {
|
||||
// Clean old requests
|
||||
validRequests := []int64{}
|
||||
for _, reqTime := range r.requests[clientID] {
|
||||
if now-reqTime <= r.window {
|
||||
validRequests = append(validRequests, reqTime)
|
||||
}
|
||||
}
|
||||
|
||||
if len(validRequests) >= r.limit {
|
||||
r.requests[clientID] = validRequests
|
||||
return false
|
||||
}
|
||||
|
||||
validRequests = append(validRequests, now)
|
||||
r.requests[clientID] = validRequests
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package security
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestInputValidator(t *testing.T) {
|
||||
v := NewInputValidator()
|
||||
|
||||
t.Run("ValidateStringLength", func(t *testing.T) {
|
||||
// Valid length
|
||||
require.NoError(t, v.ValidateStringLength("short string"))
|
||||
|
||||
// Too long
|
||||
longString := strings.Repeat("a", v.MaxStringLength+1)
|
||||
require.Error(t, v.ValidateStringLength(longString))
|
||||
})
|
||||
|
||||
t.Run("ValidateFilePath", func(t *testing.T) {
|
||||
// Use platform-specific paths
|
||||
if runtime.GOOS == "windows" {
|
||||
v.AllowedPathPrefixes = []string{"C:\\tmp", "C:\\var\\tmp"}
|
||||
|
||||
// Valid paths
|
||||
require.NoError(t, v.ValidateFilePath("C:\\tmp\\test.txt"))
|
||||
require.NoError(t, v.ValidateFilePath("C:\\var\\tmp\\data.log"))
|
||||
|
||||
// Path traversal attempts
|
||||
require.Error(t, v.ValidateFilePath("C:\\tmp\\..\\etc\\passwd"))
|
||||
require.Error(t, v.ValidateFilePath("..\\..\\etc\\passwd"))
|
||||
|
||||
// Not absolute
|
||||
require.Error(t, v.ValidateFilePath("relative\\path.txt"))
|
||||
|
||||
// Outside allowed prefixes
|
||||
require.Error(t, v.ValidateFilePath("C:\\etc\\passwd"))
|
||||
require.Error(t, v.ValidateFilePath("C:\\Users\\user\\file.txt"))
|
||||
} else {
|
||||
v.AllowedPathPrefixes = []string{"/tmp", "/var/tmp"}
|
||||
|
||||
// Valid paths
|
||||
require.NoError(t, v.ValidateFilePath("/tmp/test.txt"))
|
||||
require.NoError(t, v.ValidateFilePath("/var/tmp/data.log"))
|
||||
|
||||
// Path traversal attempts
|
||||
require.Error(t, v.ValidateFilePath("/tmp/../etc/passwd"))
|
||||
require.Error(t, v.ValidateFilePath("../../etc/passwd"))
|
||||
|
||||
// Not absolute
|
||||
require.Error(t, v.ValidateFilePath("relative/path.txt"))
|
||||
|
||||
// Outside allowed prefixes
|
||||
require.Error(t, v.ValidateFilePath("/etc/passwd"))
|
||||
require.Error(t, v.ValidateFilePath("/home/user/file.txt"))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ValidateIPAddress", func(t *testing.T) {
|
||||
// Valid IPs
|
||||
require.NoError(t, v.ValidateIPAddress("192.168.1.1"))
|
||||
require.NoError(t, v.ValidateIPAddress("10.0.0.1"))
|
||||
require.NoError(t, v.ValidateIPAddress("::1"))
|
||||
require.NoError(t, v.ValidateIPAddress("2001:db8::1"))
|
||||
|
||||
// Valid hostnames
|
||||
require.NoError(t, v.ValidateIPAddress("example.com"))
|
||||
require.NoError(t, v.ValidateIPAddress("sub.domain.example.com"))
|
||||
require.NoError(t, v.ValidateIPAddress("localhost"))
|
||||
|
||||
// With port
|
||||
require.NoError(t, v.ValidateIPAddress("192.168.1.1:8080"))
|
||||
require.NoError(t, v.ValidateIPAddress("example.com:443"))
|
||||
|
||||
// Invalid
|
||||
require.Error(t, v.ValidateIPAddress("not_a_valid-host!"))
|
||||
require.Error(t, v.ValidateIPAddress("192.168.1.256")) // Invalid octet
|
||||
require.Error(t, v.ValidateIPAddress(strings.Repeat("a", 254))) // Too long
|
||||
})
|
||||
|
||||
t.Run("ValidatePort", func(t *testing.T) {
|
||||
// Valid ports
|
||||
require.NoError(t, v.ValidatePort(80))
|
||||
require.NoError(t, v.ValidatePort(443))
|
||||
require.NoError(t, v.ValidatePort(8080))
|
||||
require.NoError(t, v.ValidatePort(65535))
|
||||
|
||||
// Invalid ports
|
||||
require.Error(t, v.ValidatePort(0))
|
||||
require.Error(t, v.ValidatePort(-1))
|
||||
require.Error(t, v.ValidatePort(65536))
|
||||
require.Error(t, v.ValidatePort(100000))
|
||||
})
|
||||
}
|
||||
|
||||
func TestSafeIntConversion(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value int64
|
||||
bitSize int
|
||||
wantErr bool
|
||||
}{
|
||||
{"int8 valid", 127, 8, false},
|
||||
{"int8 overflow", 128, 8, true},
|
||||
{"int8 underflow", -129, 8, true},
|
||||
{"int16 valid", 32767, 16, false},
|
||||
{"int16 overflow", 32768, 16, true},
|
||||
{"int32 valid", 2147483647, 32, false},
|
||||
{"int32 overflow", 2147483648, 32, true},
|
||||
{"int64 valid", 9223372036854775807, 64, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := SafeIntConversion(tt.value, tt.bitSize)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSafeUintConversion(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value int64
|
||||
bitSize int
|
||||
wantErr bool
|
||||
}{
|
||||
{"uint8 valid", 255, 8, false},
|
||||
{"uint8 overflow", 256, 8, true},
|
||||
{"uint8 negative", -1, 8, true},
|
||||
{"uint16 valid", 65535, 16, false},
|
||||
{"uint16 overflow", 65536, 16, true},
|
||||
{"uint32 valid", 4294967295, 32, false},
|
||||
{"uint32 overflow", 4294967296, 32, true},
|
||||
{"uint64 valid", 9223372036854775807, 64, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := SafeUintConversion(tt.value, tt.bitSize)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConstantTimeCompare(t *testing.T) {
|
||||
tests := []struct {
|
||||
a, b string
|
||||
expected bool
|
||||
}{
|
||||
{"password", "password", true},
|
||||
{"password", "Password", false},
|
||||
{"short", "longer", false},
|
||||
{"", "", true},
|
||||
{"abc", "def", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := ConstantTimeCompare(tt.a, tt.b)
|
||||
require.Equal(t, tt.expected, result, "Comparing %q and %q", tt.a, tt.b)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeSQL(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"normal input", "normal input"},
|
||||
{"input; DROP TABLE users", "input DROP TABLE users"},
|
||||
{"input--comment", "inputcomment"},
|
||||
{"input /* comment */", "input comment "},
|
||||
{"input'quote", "input''quote"},
|
||||
{`input"doublequote`, `input""doublequote`},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := SanitizeSQL(tt.input)
|
||||
require.Equal(t, tt.expected, result, "Sanitizing %q", tt.input)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAlphanumeric(t *testing.T) {
|
||||
// Valid
|
||||
require.NoError(t, ValidateAlphanumeric("abc123"))
|
||||
require.NoError(t, ValidateAlphanumeric("ABC"))
|
||||
require.NoError(t, ValidateAlphanumeric("123"))
|
||||
|
||||
// Invalid
|
||||
require.Error(t, ValidateAlphanumeric("abc-123"))
|
||||
require.Error(t, ValidateAlphanumeric("abc_123"))
|
||||
require.Error(t, ValidateAlphanumeric("abc 123"))
|
||||
require.Error(t, ValidateAlphanumeric("abc@123"))
|
||||
}
|
||||
|
||||
func TestValidateEmail(t *testing.T) {
|
||||
// Valid
|
||||
require.NoError(t, ValidateEmail("user@example.com"))
|
||||
require.NoError(t, ValidateEmail("user.name@example.com"))
|
||||
require.NoError(t, ValidateEmail("user+tag@example.co.uk"))
|
||||
|
||||
// Invalid
|
||||
require.Error(t, ValidateEmail("notanemail"))
|
||||
require.Error(t, ValidateEmail("@example.com"))
|
||||
require.Error(t, ValidateEmail("user@"))
|
||||
require.Error(t, ValidateEmail("user@.com"))
|
||||
require.Error(t, ValidateEmail(strings.Repeat("a", 250)+"@example.com"))
|
||||
}
|
||||
|
||||
func TestValidateURL(t *testing.T) {
|
||||
// Valid
|
||||
require.NoError(t, ValidateURL("http://example.com"))
|
||||
require.NoError(t, ValidateURL("https://example.com"))
|
||||
require.NoError(t, ValidateURL("https://example.com/path"))
|
||||
|
||||
// Invalid - dangerous patterns
|
||||
require.Error(t, ValidateURL("javascript:alert(1)"))
|
||||
require.Error(t, ValidateURL("data:text/html,<script>alert(1)</script>"))
|
||||
require.Error(t, ValidateURL("vbscript:msgbox"))
|
||||
require.Error(t, ValidateURL("file:///etc/passwd"))
|
||||
|
||||
// Invalid - wrong protocol
|
||||
require.Error(t, ValidateURL("ftp://example.com"))
|
||||
require.Error(t, ValidateURL("example.com"))
|
||||
}
|
||||
|
||||
func TestRateLimiter(t *testing.T) {
|
||||
rl := NewRateLimiter(3, 60) // 3 requests per 60 seconds
|
||||
clientID := "test-client"
|
||||
now := time.Now().Unix()
|
||||
|
||||
// First 3 requests should be allowed
|
||||
require.True(t, rl.Allow(clientID, now))
|
||||
require.True(t, rl.Allow(clientID, now+1))
|
||||
require.True(t, rl.Allow(clientID, now+2))
|
||||
|
||||
// 4th request should be blocked
|
||||
require.False(t, rl.Allow(clientID, now+3))
|
||||
|
||||
// After window expires, should allow again
|
||||
require.True(t, rl.Allow(clientID, now+61))
|
||||
}
|
||||
Reference in New Issue
Block a user