To gitea and beyond, let's go(-yco)
This commit is contained in:
140
internal/security/fuzz_test.go
Normal file
140
internal/security/fuzz_test.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"goyco/internal/fuzz"
|
||||
"goyco/internal/testutils"
|
||||
)
|
||||
|
||||
func FuzzSanitizeInput(f *testing.F) {
|
||||
helper := fuzz.NewFuzzTestHelper()
|
||||
helper.RunSanitizationFuzzTest(f, func(input string) string {
|
||||
result := SanitizeInput(input)
|
||||
testutils.ValidateNoScriptTags(result)
|
||||
testutils.ValidateNoJavascriptProtocol(result)
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
func FuzzSanitizeUsername(f *testing.F) {
|
||||
helper := fuzz.NewFuzzTestHelper()
|
||||
helper.RunSanitizationFuzzTest(f, func(input string) string {
|
||||
result := SanitizeUsername(input)
|
||||
testutils.ValidateNoDangerousChars(result)
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
func FuzzSanitizeEmail(f *testing.F) {
|
||||
helper := fuzz.NewFuzzTestHelper()
|
||||
helper.RunBasicFuzzTest(f, func(t *testing.T, input string) {
|
||||
result := SanitizeEmail(input)
|
||||
if result != "" {
|
||||
testutils.ValidateUTF8String(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func FuzzSanitizePostContent(f *testing.F) {
|
||||
helper := fuzz.NewFuzzTestHelper()
|
||||
helper.RunSanitizationFuzzTest(f, func(input string) string {
|
||||
result := SanitizePostContent(input)
|
||||
testutils.ValidateNoScriptTags(result)
|
||||
testutils.ValidateNoJavascriptProtocol(result)
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
func FuzzSanitizeURL(f *testing.F) {
|
||||
helper := fuzz.NewFuzzTestHelper()
|
||||
helper.RunBasicFuzzTest(f, func(t *testing.T, input string) {
|
||||
result := SanitizeURL(input)
|
||||
if result != "" {
|
||||
testutils.ValidateUTF8String(result)
|
||||
testutils.ValidateNoPrivateIPs(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func FuzzInputSanitizerUsernameCLI(f *testing.F) {
|
||||
helper := fuzz.NewFuzzTestHelper()
|
||||
helper.RunBasicFuzzTest(f, func(t *testing.T, input string) {
|
||||
sanitizer := NewInputSanitizer()
|
||||
result, err := sanitizer.SanitizeUsernameCLI(input)
|
||||
if err == nil {
|
||||
testutils.ValidateUTF8String(result)
|
||||
testutils.ValidateNoDangerousChars(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func FuzzInputSanitizerEmailCLI(f *testing.F) {
|
||||
helper := fuzz.NewFuzzTestHelper()
|
||||
helper.RunBasicFuzzTest(f, func(t *testing.T, input string) {
|
||||
sanitizer := NewInputSanitizer()
|
||||
result, err := sanitizer.SanitizeEmailCLI(input)
|
||||
if err == nil {
|
||||
testutils.ValidateUTF8String(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func FuzzInputSanitizerPasswordCLI(f *testing.F) {
|
||||
helper := fuzz.NewFuzzTestHelper()
|
||||
helper.RunBasicFuzzTest(f, func(t *testing.T, input string) {
|
||||
sanitizer := NewInputSanitizer()
|
||||
_ = sanitizer.SanitizePasswordCLI(input)
|
||||
})
|
||||
}
|
||||
|
||||
func FuzzInputSanitizerSearchTerm(f *testing.F) {
|
||||
helper := fuzz.NewFuzzTestHelper()
|
||||
helper.RunBasicFuzzTest(f, func(t *testing.T, input string) {
|
||||
sanitizer := NewInputSanitizer()
|
||||
result, err := sanitizer.SanitizeSearchTerm(input)
|
||||
if err == nil {
|
||||
testutils.ValidateUTF8String(result)
|
||||
testutils.ValidateNoDangerousChars(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func FuzzInputSanitizerTitleCLI(f *testing.F) {
|
||||
helper := fuzz.NewFuzzTestHelper()
|
||||
helper.RunBasicFuzzTest(f, func(t *testing.T, input string) {
|
||||
sanitizer := NewInputSanitizer()
|
||||
result, err := sanitizer.SanitizeTitleCLI(input)
|
||||
if err == nil {
|
||||
testutils.ValidateUTF8String(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func FuzzInputSanitizerContentCLI(f *testing.F) {
|
||||
helper := fuzz.NewFuzzTestHelper()
|
||||
helper.RunBasicFuzzTest(f, func(t *testing.T, input string) {
|
||||
sanitizer := NewInputSanitizer()
|
||||
result, err := sanitizer.SanitizeContentCLI(input)
|
||||
if err == nil {
|
||||
testutils.ValidateUTF8String(result)
|
||||
testutils.ValidateNoDangerousHTMLTags(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func FuzzInputSanitizerID(f *testing.F) {
|
||||
helper := fuzz.NewFuzzTestHelper()
|
||||
helper.RunBasicFuzzTest(f, func(t *testing.T, input string) {
|
||||
sanitizer := NewInputSanitizer()
|
||||
result, err := sanitizer.SanitizeID(input)
|
||||
if err == nil {
|
||||
if result == 0 {
|
||||
t.Fatal("SanitizeID returned 0 for valid input")
|
||||
}
|
||||
if result > 1000000 {
|
||||
t.Fatal("SanitizeID returned ID larger than expected limit")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
422
internal/security/sanitizer.go
Normal file
422
internal/security/sanitizer.go
Normal file
@@ -0,0 +1,422 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
func SanitizeInput(input string) string {
|
||||
|
||||
if !utf8.ValidString(input) {
|
||||
input = strings.ToValidUTF8(input, "")
|
||||
}
|
||||
|
||||
cleaned := strings.TrimSpace(input)
|
||||
|
||||
cleaned = html.EscapeString(cleaned)
|
||||
|
||||
scriptRegex := regexp.MustCompile(`(?i)<script[^>]*>.*?</script>`)
|
||||
cleaned = scriptRegex.ReplaceAllString(cleaned, "")
|
||||
|
||||
jsRegex := regexp.MustCompile(`(?i)javascript:`)
|
||||
cleaned = jsRegex.ReplaceAllString(cleaned, "")
|
||||
|
||||
eventRegex := regexp.MustCompile(`(?i)\son\w+\s*=\s*"[^"]*"`)
|
||||
cleaned = eventRegex.ReplaceAllString(cleaned, "")
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
func SanitizeUsername(username string) string {
|
||||
cleaned := SanitizeInput(username)
|
||||
|
||||
reg := regexp.MustCompile(`[^a-zA-Z0-9_-]`)
|
||||
cleaned = reg.ReplaceAllString(cleaned, "")
|
||||
|
||||
if len(cleaned) > 0 && !unicode.IsLetter(rune(cleaned[0])) && !unicode.IsDigit(rune(cleaned[0])) {
|
||||
cleaned = "user_" + cleaned
|
||||
}
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
func SanitizeEmail(email string) string {
|
||||
cleaned := SanitizeInput(email)
|
||||
|
||||
cleaned = strings.ToLower(cleaned)
|
||||
|
||||
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
|
||||
if !emailRegex.MatchString(cleaned) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
func SanitizePostContent(content string) string {
|
||||
|
||||
if !utf8.ValidString(content) {
|
||||
content = strings.ToValidUTF8(content, "")
|
||||
}
|
||||
|
||||
cleaned := strings.TrimSpace(content)
|
||||
cleaned = html.EscapeString(cleaned)
|
||||
|
||||
scriptRegex := regexp.MustCompile(`(?i)<script[^>]*>.*?</script>`)
|
||||
cleaned = scriptRegex.ReplaceAllString(cleaned, "")
|
||||
|
||||
jsRegex := regexp.MustCompile(`(?i)javascript:`)
|
||||
cleaned = jsRegex.ReplaceAllString(cleaned, "")
|
||||
|
||||
eventRegex := regexp.MustCompile(`(?i)\son\w+\s*=\s*"[^"]*"`)
|
||||
cleaned = eventRegex.ReplaceAllString(cleaned, "")
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
func SanitizeURL(url string) string {
|
||||
|
||||
if !utf8.ValidString(url) {
|
||||
url = strings.ToValidUTF8(url, "")
|
||||
}
|
||||
|
||||
cleaned := strings.TrimSpace(url)
|
||||
|
||||
if !strings.HasPrefix(cleaned, "http://") && !strings.HasPrefix(cleaned, "https://") {
|
||||
return ""
|
||||
}
|
||||
|
||||
privateIPRegex := regexp.MustCompile(`(?i)(localhost|127\.0\.0\.1|0\.0\.0\.0|10\.|172\.(1[6-9]|2[0-9]|3[0-1])\.|192\.168\.)`)
|
||||
if privateIPRegex.MatchString(cleaned) {
|
||||
return ""
|
||||
}
|
||||
|
||||
if strings.Contains(cleaned, "169.254.169.254") {
|
||||
return ""
|
||||
}
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
type InputSanitizer struct {
|
||||
MaxUsernameLength int
|
||||
MaxEmailLength int
|
||||
MaxTitleLength int
|
||||
MaxContentLength int
|
||||
MaxSearchLength int
|
||||
}
|
||||
|
||||
func NewInputSanitizer() *InputSanitizer {
|
||||
return &InputSanitizer{
|
||||
MaxUsernameLength: 50,
|
||||
MaxEmailLength: 100,
|
||||
MaxTitleLength: 200,
|
||||
MaxContentLength: 5000,
|
||||
MaxSearchLength: 100,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *InputSanitizer) SanitizeUsernameCLI(username string) (string, error) {
|
||||
if username == "" {
|
||||
return "", fmt.Errorf("username cannot be empty")
|
||||
}
|
||||
|
||||
username = strings.TrimSpace(username)
|
||||
|
||||
if len(username) > s.MaxUsernameLength {
|
||||
return "", fmt.Errorf("username must be %d characters or less", s.MaxUsernameLength)
|
||||
}
|
||||
|
||||
if len(username) < 3 {
|
||||
return "", fmt.Errorf("username must be at least 3 characters")
|
||||
}
|
||||
|
||||
validUsernameRegex := regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
|
||||
if !validUsernameRegex.MatchString(username) {
|
||||
return "", fmt.Errorf("username can only contain letters, numbers, underscores, and hyphens")
|
||||
}
|
||||
|
||||
reservedUsernames := []string{
|
||||
"admin", "administrator", "root", "system", "api", "www", "mail",
|
||||
"ftp", "localhost", "test", "demo", "guest", "user", "support",
|
||||
"help", "info", "contact", "about", "privacy", "terms", "login",
|
||||
"register", "signup", "signin", "logout", "profile", "settings",
|
||||
"account", "dashboard", "home", "index", "default", "null", "undefined",
|
||||
}
|
||||
|
||||
lowerUsername := strings.ToLower(username)
|
||||
if slices.Contains(reservedUsernames, lowerUsername) {
|
||||
return "", fmt.Errorf("username '%s' is reserved and cannot be used", username)
|
||||
}
|
||||
|
||||
if strings.Contains(username, "__") || strings.Contains(username, "--") {
|
||||
return "", fmt.Errorf("username cannot contain consecutive underscores or hyphens")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(username, "_") || strings.HasPrefix(username, "-") ||
|
||||
strings.HasSuffix(username, "_") || strings.HasSuffix(username, "-") {
|
||||
return "", fmt.Errorf("username cannot start or end with underscore or hyphen")
|
||||
}
|
||||
|
||||
return username, nil
|
||||
}
|
||||
|
||||
func (s *InputSanitizer) SanitizeEmailCLI(email string) (string, error) {
|
||||
if email == "" {
|
||||
return "", fmt.Errorf("email cannot be empty")
|
||||
}
|
||||
|
||||
email = strings.TrimSpace(strings.ToLower(email))
|
||||
|
||||
if len(email) > s.MaxEmailLength {
|
||||
return "", fmt.Errorf("email must be %d characters or less", s.MaxEmailLength)
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
suspiciousPatterns := []string{
|
||||
"..", "++", "--", "__", "%%",
|
||||
}
|
||||
|
||||
for _, pattern := range suspiciousPatterns {
|
||||
if strings.Contains(email, pattern) {
|
||||
return "", fmt.Errorf("email contains invalid characters")
|
||||
}
|
||||
}
|
||||
|
||||
dotCount := strings.Count(email, ".")
|
||||
if dotCount > 3 {
|
||||
return "", fmt.Errorf("email contains too many dots")
|
||||
}
|
||||
|
||||
return email, nil
|
||||
}
|
||||
|
||||
func (s *InputSanitizer) SanitizePasswordCLI(password string) error {
|
||||
if password == "" {
|
||||
return fmt.Errorf("password cannot be empty")
|
||||
}
|
||||
|
||||
if len(password) < 8 {
|
||||
return fmt.Errorf("password must be at least 8 characters")
|
||||
}
|
||||
|
||||
if len(password) > 128 {
|
||||
return fmt.Errorf("password must be 128 characters or less")
|
||||
}
|
||||
|
||||
weakPasswords := []string{
|
||||
"password", "123456", "12345678", "qwerty", "abc123",
|
||||
"password123", "admin", "letmein", "welcome", "monkey",
|
||||
"dragon", "master", "hello", "login", "princess",
|
||||
}
|
||||
|
||||
lowerPassword := strings.ToLower(password)
|
||||
if slices.Contains(weakPasswords, lowerPassword) {
|
||||
return fmt.Errorf("password is too common, please choose a stronger password")
|
||||
}
|
||||
|
||||
hasUpper := false
|
||||
hasLower := false
|
||||
hasDigit := false
|
||||
hasSpecial := false
|
||||
|
||||
for _, char := range password {
|
||||
switch {
|
||||
case unicode.IsUpper(char):
|
||||
hasUpper = true
|
||||
case unicode.IsLower(char):
|
||||
hasLower = true
|
||||
case unicode.IsDigit(char):
|
||||
hasDigit = true
|
||||
case unicode.IsPunct(char) || unicode.IsSymbol(char):
|
||||
hasSpecial = true
|
||||
}
|
||||
}
|
||||
|
||||
countBools := func(bools ...bool) int {
|
||||
count := 0
|
||||
for _, b := range bools {
|
||||
if b {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
if countBools(hasUpper, hasLower, hasDigit, hasSpecial) < 3 {
|
||||
return fmt.Errorf("password must contain at least 3 different character types (uppercase, lowercase, digits, special characters)")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *InputSanitizer) SanitizeSearchTerm(term string) (string, error) {
|
||||
if term == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if !utf8.ValidString(term) {
|
||||
term = strings.ToValidUTF8(term, "")
|
||||
}
|
||||
|
||||
term = strings.TrimSpace(term)
|
||||
|
||||
if len(term) > s.MaxSearchLength {
|
||||
return "", fmt.Errorf("search term must be %d characters or less", s.MaxSearchLength)
|
||||
}
|
||||
|
||||
dangerousChars := []string{
|
||||
"<", ">", "\"", "'", "&", "|", ";", "`", "$", "(", ")", "{", "}",
|
||||
"[", "]", "\\", "/", "*", "?", "!", "@", "#", "%", "^", "~",
|
||||
}
|
||||
|
||||
for _, char := range dangerousChars {
|
||||
term = strings.ReplaceAll(term, char, "")
|
||||
}
|
||||
|
||||
term = regexp.MustCompile(`\s+`).ReplaceAllString(term, " ")
|
||||
|
||||
if s.hasExcessiveRepetition(term) {
|
||||
return "", fmt.Errorf("search term contains excessive repetition")
|
||||
}
|
||||
|
||||
return term, nil
|
||||
}
|
||||
|
||||
func (s *InputSanitizer) SanitizeTitleCLI(title string) (string, error) {
|
||||
if title == "" {
|
||||
return "", fmt.Errorf("title cannot be empty")
|
||||
}
|
||||
|
||||
if !utf8.ValidString(title) {
|
||||
title = strings.ToValidUTF8(title, "")
|
||||
}
|
||||
|
||||
title = strings.TrimSpace(title)
|
||||
|
||||
if len(title) > s.MaxTitleLength {
|
||||
return "", fmt.Errorf("title must be %d characters or less", s.MaxTitleLength)
|
||||
}
|
||||
|
||||
if len(title) < 3 {
|
||||
return "", fmt.Errorf("title must be at least 3 characters")
|
||||
}
|
||||
|
||||
htmlTagRegex := regexp.MustCompile(`<[^>]*>`)
|
||||
title = htmlTagRegex.ReplaceAllString(title, "")
|
||||
|
||||
title = regexp.MustCompile(`\s+`).ReplaceAllString(title, " ")
|
||||
|
||||
return title, nil
|
||||
}
|
||||
|
||||
func (s *InputSanitizer) SanitizeContentCLI(content string) (string, error) {
|
||||
if content == "" {
|
||||
return "", fmt.Errorf("content cannot be empty")
|
||||
}
|
||||
|
||||
if !utf8.ValidString(content) {
|
||||
content = strings.ToValidUTF8(content, "")
|
||||
}
|
||||
|
||||
content = strings.TrimSpace(content)
|
||||
|
||||
if len(content) > s.MaxContentLength {
|
||||
return "", fmt.Errorf("content must be %d characters or less", s.MaxContentLength)
|
||||
}
|
||||
|
||||
if len(content) < 10 {
|
||||
return "", fmt.Errorf("content must be at least 10 characters")
|
||||
}
|
||||
|
||||
dangerousTags := []string{
|
||||
"<script", "</script>", "<iframe", "</iframe>", "<object", "</object>",
|
||||
"<embed", "</embed>", "<form", "</form>", "<input", "<button",
|
||||
"<link", "<meta", "<style", "</style>",
|
||||
}
|
||||
|
||||
for _, tag := range dangerousTags {
|
||||
content = regexp.MustCompile(`(?i)`+regexp.QuoteMeta(tag)).ReplaceAllString(content, "")
|
||||
}
|
||||
|
||||
content = regexp.MustCompile(`\s+`).ReplaceAllString(content, " ")
|
||||
|
||||
return content, nil
|
||||
}
|
||||
|
||||
func (s *InputSanitizer) hasExcessiveRepetition(text string) bool {
|
||||
if s.hasRepeatedCharacters(text, 5) {
|
||||
return true
|
||||
}
|
||||
|
||||
words := strings.Fields(text)
|
||||
wordCount := make(map[string]int)
|
||||
for _, word := range words {
|
||||
wordCount[strings.ToLower(word)]++
|
||||
if wordCount[strings.ToLower(word)] > 3 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *InputSanitizer) hasRepeatedCharacters(str string, maxRepeats int) bool {
|
||||
if len(str) <= maxRepeats {
|
||||
return false
|
||||
}
|
||||
|
||||
currentChar := rune(0)
|
||||
count := 0
|
||||
|
||||
for _, char := range str {
|
||||
if char == currentChar {
|
||||
count++
|
||||
if count > maxRepeats {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
currentChar = char
|
||||
count = 1
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *InputSanitizer) SanitizeID(idStr string) (uint, error) {
|
||||
if idStr == "" {
|
||||
return 0, fmt.Errorf("ID cannot be empty")
|
||||
}
|
||||
|
||||
idStr = strings.TrimSpace(idStr)
|
||||
|
||||
if !regexp.MustCompile(`^\d+$`).MatchString(idStr) {
|
||||
return 0, fmt.Errorf("ID must be a positive integer")
|
||||
}
|
||||
|
||||
var id uint
|
||||
_, err := fmt.Sscanf(idStr, "%d", &id)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid ID format: %s", idStr)
|
||||
}
|
||||
|
||||
if id == 0 {
|
||||
return 0, fmt.Errorf("ID must be greater than 0")
|
||||
}
|
||||
|
||||
if id > 1000000 {
|
||||
return 0, fmt.Errorf("ID is too large")
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
600
internal/security/sanitizer_test.go
Normal file
600
internal/security/sanitizer_test.go
Normal file
@@ -0,0 +1,600 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSanitizeInput(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "basic text",
|
||||
input: "Hello World",
|
||||
expected: "Hello World",
|
||||
},
|
||||
{
|
||||
name: "script tag removal",
|
||||
input: "<script>alert('xss')</script>Hello",
|
||||
expected: "<script>alert('xss')</script>Hello",
|
||||
},
|
||||
{
|
||||
name: "javascript protocol removal",
|
||||
input: "javascript:alert('xss')",
|
||||
expected: "alert('xss')",
|
||||
},
|
||||
{
|
||||
name: "event handler removal",
|
||||
input: "<img src='x' onerror='alert(1)'>",
|
||||
expected: "<img src='x' onerror='alert(1)'>",
|
||||
},
|
||||
{
|
||||
name: "mixed content",
|
||||
input: "Hello <script>alert('xss')</script> World",
|
||||
expected: "Hello <script>alert('xss')</script> World",
|
||||
},
|
||||
{
|
||||
name: "whitespace trimming",
|
||||
input: " Hello World ",
|
||||
expected: "Hello World",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := SanitizeInput(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("SanitizeInput(%q) = %q, expected %q", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeUsername(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "valid username",
|
||||
input: "testuser",
|
||||
expected: "testuser",
|
||||
},
|
||||
{
|
||||
name: "username with special chars",
|
||||
input: "test_user-123",
|
||||
expected: "test_user-123",
|
||||
},
|
||||
{
|
||||
name: "username with invalid chars",
|
||||
input: "test@user#123",
|
||||
expected: "testuser123",
|
||||
},
|
||||
{
|
||||
name: "username starting with number",
|
||||
input: "123test",
|
||||
expected: "123test",
|
||||
},
|
||||
{
|
||||
name: "username starting with special char",
|
||||
input: "@testuser",
|
||||
expected: "testuser",
|
||||
},
|
||||
{
|
||||
name: "empty username",
|
||||
input: "",
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := SanitizeUsername(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("SanitizeUsername(%q) = %q, expected %q", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeEmail(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "valid email",
|
||||
input: "test@example.com",
|
||||
expected: "test@example.com",
|
||||
},
|
||||
{
|
||||
name: "email with uppercase",
|
||||
input: "TEST@EXAMPLE.COM",
|
||||
expected: "test@example.com",
|
||||
},
|
||||
{
|
||||
name: "invalid email",
|
||||
input: "not-an-email",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "email with script",
|
||||
input: "test<script>@example.com",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "empty email",
|
||||
input: "",
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := SanitizeEmail(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("SanitizeEmail(%q) = %q, expected %q", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "valid https url",
|
||||
input: "https://example.com",
|
||||
expected: "https://example.com",
|
||||
},
|
||||
{
|
||||
name: "valid http url",
|
||||
input: "http://example.com",
|
||||
expected: "http://example.com",
|
||||
},
|
||||
{
|
||||
name: "invalid protocol",
|
||||
input: "ftp://example.com",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "localhost blocked",
|
||||
input: "http://localhost:8080",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "private ip blocked",
|
||||
input: "http://192.168.1.1",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "aws metadata blocked",
|
||||
input: "http://169.254.169.254/latest/meta-data",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "empty url",
|
||||
input: "",
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := SanitizeURL(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("SanitizeURL(%q) = %q, expected %q", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInputSanitizer_SanitizePasswordCLI(t *testing.T) {
|
||||
sanitizer := &InputSanitizer{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
password string
|
||||
expectError bool
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "valid_password_with_all_types",
|
||||
password: "Password123!",
|
||||
expectError: false,
|
||||
description: "Password with uppercase, lowercase, numbers, and special chars should be valid",
|
||||
},
|
||||
{
|
||||
name: "valid_password_with_underscore",
|
||||
password: "Password123_",
|
||||
expectError: false,
|
||||
description: "Password with underscore should be valid",
|
||||
},
|
||||
{
|
||||
name: "valid_password_with_hyphen",
|
||||
password: "Password123-",
|
||||
expectError: false,
|
||||
description: "Password with hyphen should be valid",
|
||||
},
|
||||
{
|
||||
name: "valid_password_minimum_length",
|
||||
password: "Pass123!",
|
||||
expectError: false,
|
||||
description: "Password with exactly 8 characters should be valid",
|
||||
},
|
||||
{
|
||||
name: "valid_password_maximum_length",
|
||||
password: createValidCLIPassword(128),
|
||||
expectError: false,
|
||||
description: "Password with exactly 128 characters should be valid",
|
||||
},
|
||||
|
||||
{
|
||||
name: "empty_password",
|
||||
password: "",
|
||||
expectError: true,
|
||||
description: "Empty password should be rejected",
|
||||
},
|
||||
|
||||
{
|
||||
name: "password_too_short",
|
||||
password: "Pass1!",
|
||||
expectError: true,
|
||||
description: "Password shorter than 8 characters should be rejected",
|
||||
},
|
||||
{
|
||||
name: "password_too_long",
|
||||
password: string(make([]byte, 129)),
|
||||
expectError: true,
|
||||
description: "Password longer than 128 characters should be rejected",
|
||||
},
|
||||
|
||||
{
|
||||
name: "common_weak_password",
|
||||
password: "password",
|
||||
expectError: true,
|
||||
description: "Common weak password should be rejected",
|
||||
},
|
||||
{
|
||||
name: "common_weak_password_123456",
|
||||
password: "123456",
|
||||
expectError: true,
|
||||
description: "Common weak password 123456 should be rejected",
|
||||
},
|
||||
{
|
||||
name: "common_weak_password_12345678",
|
||||
password: "12345678",
|
||||
expectError: true,
|
||||
description: "Common weak password 12345678 should be rejected",
|
||||
},
|
||||
{
|
||||
name: "common_weak_password_qwerty",
|
||||
password: "qwerty",
|
||||
expectError: true,
|
||||
description: "Common weak password qwerty should be rejected",
|
||||
},
|
||||
{
|
||||
name: "common_weak_password_abc123",
|
||||
password: "abc123",
|
||||
expectError: true,
|
||||
description: "Common weak password abc123 should be rejected",
|
||||
},
|
||||
{
|
||||
name: "common_weak_password_password123",
|
||||
password: "password123",
|
||||
expectError: true,
|
||||
description: "Common weak password password123 should be rejected",
|
||||
},
|
||||
{
|
||||
name: "common_weak_password_admin",
|
||||
password: "admin",
|
||||
expectError: true,
|
||||
description: "Common weak password admin should be rejected",
|
||||
},
|
||||
{
|
||||
name: "common_weak_password_letmein",
|
||||
password: "letmein",
|
||||
expectError: true,
|
||||
description: "Common weak password letmein should be rejected",
|
||||
},
|
||||
{
|
||||
name: "common_weak_password_welcome",
|
||||
password: "welcome",
|
||||
expectError: true,
|
||||
description: "Common weak password welcome should be rejected",
|
||||
},
|
||||
{
|
||||
name: "common_weak_password_monkey",
|
||||
password: "monkey",
|
||||
expectError: true,
|
||||
description: "Common weak password monkey should be rejected",
|
||||
},
|
||||
{
|
||||
name: "common_weak_password_dragon",
|
||||
password: "dragon",
|
||||
expectError: true,
|
||||
description: "Common weak password dragon should be rejected",
|
||||
},
|
||||
{
|
||||
name: "common_weak_password_master",
|
||||
password: "master",
|
||||
expectError: true,
|
||||
description: "Common weak password master should be rejected",
|
||||
},
|
||||
{
|
||||
name: "common_weak_password_hello",
|
||||
password: "hello",
|
||||
expectError: true,
|
||||
description: "Common weak password hello should be rejected",
|
||||
},
|
||||
{
|
||||
name: "common_weak_password_login",
|
||||
password: "login",
|
||||
expectError: true,
|
||||
description: "Common weak password login should be rejected",
|
||||
},
|
||||
{
|
||||
name: "common_weak_password_princess",
|
||||
password: "princess",
|
||||
expectError: true,
|
||||
description: "Common weak password princess should be rejected",
|
||||
},
|
||||
|
||||
{
|
||||
name: "password_only_uppercase",
|
||||
password: "PASSWORD",
|
||||
expectError: true,
|
||||
description: "Password with only uppercase letters should be rejected",
|
||||
},
|
||||
{
|
||||
name: "password_only_lowercase",
|
||||
password: "password",
|
||||
expectError: true,
|
||||
description: "Password with only lowercase letters should be rejected",
|
||||
},
|
||||
{
|
||||
name: "password_only_numbers",
|
||||
password: "12345678",
|
||||
expectError: true,
|
||||
description: "Password with only numbers should be rejected",
|
||||
},
|
||||
{
|
||||
name: "password_only_special_chars",
|
||||
password: "!@#$%^&*",
|
||||
expectError: true,
|
||||
description: "Password with only special characters should be rejected",
|
||||
},
|
||||
{
|
||||
name: "password_uppercase_and_lowercase_only",
|
||||
password: "Password",
|
||||
expectError: true,
|
||||
description: "Password with only uppercase and lowercase should be rejected",
|
||||
},
|
||||
{
|
||||
name: "password_uppercase_and_numbers_only",
|
||||
password: "PASSWORD123",
|
||||
expectError: true,
|
||||
description: "Password with only uppercase and numbers should be rejected",
|
||||
},
|
||||
{
|
||||
name: "password_lowercase_and_numbers_only",
|
||||
password: "password123",
|
||||
expectError: true,
|
||||
description: "Password with only lowercase and numbers should be rejected",
|
||||
},
|
||||
{
|
||||
name: "password_uppercase_and_special_only",
|
||||
password: "PASSWORD!",
|
||||
expectError: true,
|
||||
description: "Password with only uppercase and special chars should be rejected",
|
||||
},
|
||||
{
|
||||
name: "password_lowercase_and_special_only",
|
||||
password: "password!",
|
||||
expectError: true,
|
||||
description: "Password with only lowercase and special chars should be rejected",
|
||||
},
|
||||
{
|
||||
name: "password_numbers_and_special_only",
|
||||
password: "12345678!",
|
||||
expectError: true,
|
||||
description: "Password with only numbers and special chars should be rejected",
|
||||
},
|
||||
|
||||
{
|
||||
name: "valid_password_3_types_upper_lower_digit",
|
||||
password: "Password123!",
|
||||
expectError: false,
|
||||
description: "Password with uppercase, lowercase, digits, and special chars should be valid",
|
||||
},
|
||||
{
|
||||
name: "valid_password_3_types_upper_lower_special",
|
||||
password: "Password!",
|
||||
expectError: false,
|
||||
description: "Password with uppercase, lowercase, and special chars should be valid",
|
||||
},
|
||||
{
|
||||
name: "valid_password_3_types_upper_digit_special",
|
||||
password: "PASSWORD123!",
|
||||
expectError: false,
|
||||
description: "Password with uppercase, digits, and special chars should be valid",
|
||||
},
|
||||
{
|
||||
name: "valid_password_3_types_lower_digit_special",
|
||||
password: "password123!",
|
||||
expectError: false,
|
||||
description: "Password with lowercase, digits, and special chars should be valid",
|
||||
},
|
||||
|
||||
{
|
||||
name: "common_weak_password_uppercase",
|
||||
password: "PASSWORD",
|
||||
expectError: true,
|
||||
description: "Common weak password in uppercase should be rejected",
|
||||
},
|
||||
{
|
||||
name: "common_weak_password_mixed_case",
|
||||
password: "PassWord",
|
||||
expectError: true,
|
||||
description: "Common weak password in mixed case should be rejected",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := sanitizer.SanitizePasswordCLI(tt.password)
|
||||
|
||||
if tt.expectError && err == nil {
|
||||
t.Errorf("SanitizePasswordCLI(%q) expected error, got nil. %s", tt.password, tt.description)
|
||||
}
|
||||
if !tt.expectError && err != nil {
|
||||
t.Errorf("SanitizePasswordCLI(%q) unexpected error: %v. %s", tt.password, err, tt.description)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInputSanitizer_SanitizePasswordCLI_Unicode(t *testing.T) {
|
||||
sanitizer := &InputSanitizer{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
password string
|
||||
expectError bool
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "unicode_letters_valid",
|
||||
password: "Pássw0rd123!",
|
||||
expectError: false,
|
||||
description: "Password with Unicode letters should be valid",
|
||||
},
|
||||
{
|
||||
name: "unicode_numbers_valid",
|
||||
password: "Password123!",
|
||||
expectError: false,
|
||||
description: "Password with Unicode numbers should be valid",
|
||||
},
|
||||
{
|
||||
name: "unicode_special_chars_valid",
|
||||
password: "Password123!",
|
||||
expectError: false,
|
||||
description: "Password with Unicode special characters should be valid",
|
||||
},
|
||||
{
|
||||
name: "mixed_unicode_valid",
|
||||
password: "Pássw0rd123!",
|
||||
expectError: false,
|
||||
description: "Password with mixed Unicode characters should be valid",
|
||||
},
|
||||
{
|
||||
name: "unicode_only_letters",
|
||||
password: "Pásswórd",
|
||||
expectError: true,
|
||||
description: "Password with only Unicode letters should be rejected",
|
||||
},
|
||||
{
|
||||
name: "unicode_only_numbers",
|
||||
password: "12345678",
|
||||
expectError: true,
|
||||
description: "Password with only Unicode numbers should be rejected",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := sanitizer.SanitizePasswordCLI(tt.password)
|
||||
|
||||
if tt.expectError && err == nil {
|
||||
t.Errorf("SanitizePasswordCLI(%q) expected error, got nil. %s", tt.password, tt.description)
|
||||
}
|
||||
if !tt.expectError && err != nil {
|
||||
t.Errorf("SanitizePasswordCLI(%q) unexpected error: %v. %s", tt.password, err, tt.description)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInputSanitizer_SanitizePasswordCLI_Boundary(t *testing.T) {
|
||||
sanitizer := &InputSanitizer{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
password string
|
||||
expectError bool
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "exactly_8_chars_valid",
|
||||
password: "Pass123!",
|
||||
expectError: false,
|
||||
description: "Password with exactly 8 characters should be valid",
|
||||
},
|
||||
{
|
||||
name: "exactly_128_chars_valid",
|
||||
password: createValidCLIPassword(128),
|
||||
expectError: false,
|
||||
description: "Password with exactly 128 characters should be valid",
|
||||
},
|
||||
{
|
||||
name: "7_chars_invalid",
|
||||
password: "Pass12!",
|
||||
expectError: true,
|
||||
description: "Password with 7 characters should be rejected",
|
||||
},
|
||||
{
|
||||
name: "129_chars_invalid",
|
||||
password: string(make([]byte, 129)),
|
||||
expectError: true,
|
||||
description: "Password with 129 characters should be rejected",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
testPassword := tt.password
|
||||
if len(testPassword) >= 8 && len(testPassword) <= 128 {
|
||||
testPassword = createValidCLIPassword(len(tt.password))
|
||||
}
|
||||
|
||||
err := sanitizer.SanitizePasswordCLI(testPassword)
|
||||
|
||||
if tt.expectError && err == nil {
|
||||
t.Errorf("SanitizePasswordCLI(%q) expected error, got nil. %s", testPassword, tt.description)
|
||||
}
|
||||
if !tt.expectError && err != nil {
|
||||
t.Errorf("SanitizePasswordCLI(%q) unexpected error: %v. %s", testPassword, err, tt.description)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func createValidCLIPassword(length int) string {
|
||||
if length < 8 {
|
||||
return "Pass123!"
|
||||
}
|
||||
|
||||
password := make([]byte, length)
|
||||
|
||||
for i := range length {
|
||||
switch i % 4 {
|
||||
case 0:
|
||||
password[i] = 'P'
|
||||
case 1:
|
||||
password[i] = 'a'
|
||||
case 2:
|
||||
password[i] = '1'
|
||||
case 3:
|
||||
password[i] = '!'
|
||||
}
|
||||
}
|
||||
|
||||
return string(password)
|
||||
}
|
||||
Reference in New Issue
Block a user