clean: squash history (binaries stripped via filter-repo)

This commit is contained in:
Hanzo AI
2026-04-13 03:45:21 -07:00
commit f6e63d60b2
2029 changed files with 465257 additions and 0 deletions
+349
View File
@@ -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)
}
}
+303
View File
@@ -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
}
+257
View File
@@ -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))
}