// Package security provides security utilities for the KnowFoolery application. package security import ( "html" "regexp" "strings" "unicode" ) const ( // PlayerNameMaxLength defines the maximum length for player names. PlayerNameMaxLength = 50 // AnswerMaxLength defines the maximum length for answer submissions. AnswerMaxLength = 500 // QuestionTextMaxLength defines the maximum length for admin-authored question text. QuestionTextMaxLength = 1000 // ThemeMaxLength defines the maximum length for theme names. ThemeMaxLength = 100 ) var ( // Sanitization order is trim -> collapse spaces -> HTML escape -> clamp -> allowed pattern. spaceRegex = regexp.MustCompile(`\s+`) // Player names allow letters, numbers, spaces, '-', '_' and '.'. playerNameAllowedPattern = regexp.MustCompile(`^[a-zA-Z0-9\s\-_.]+$`) // Themes allow letters, numbers, spaces, '-' and '_'. themeAllowedPattern = regexp.MustCompile(`^[a-zA-Z0-9\s\-_]+$`) tagRegex = regexp.MustCompile(`<[^>]*>`) scriptRegex = regexp.MustCompile(`(?i)]*>.*?`) dangerousPatterns = []string{ "javascript:", "data:", "vbscript:", " 0 { result = clampByRunes(result, opts.MaxLength) } // Validate against allowed pattern if opts.AllowedPattern != nil && !opts.AllowedPattern.MatchString(result) { return "" } return result } // SanitizePlayerName sanitizes a player name. func SanitizePlayerName(input string) string { opts := SanitizeOptions{ TrimWhitespace: true, RemoveMultipleSpaces: true, HTMLEscape: true, MaxLength: PlayerNameMaxLength, AllowedPattern: playerNameAllowedPattern, } return Sanitize(input, opts) } // SanitizeAnswer sanitizes an answer submission. func SanitizeAnswer(input string) string { opts := SanitizeOptions{ TrimWhitespace: true, RemoveMultipleSpaces: true, HTMLEscape: true, MaxLength: AnswerMaxLength, } result := Sanitize(input, opts) // Normalize to lowercase for comparison result = strings.ToLower(result) return result } // SanitizeQuestionText sanitizes question text (admin input). func SanitizeQuestionText(input string) string { opts := SanitizeOptions{ TrimWhitespace: true, RemoveMultipleSpaces: true, HTMLEscape: true, MaxLength: QuestionTextMaxLength, } result := Sanitize(input, opts) // Remove potential script content result = scriptRegex.ReplaceAllString(result, "") return result } // SanitizeTheme sanitizes a theme name. func SanitizeTheme(input string) string { opts := SanitizeOptions{ TrimWhitespace: true, RemoveMultipleSpaces: true, HTMLEscape: true, MaxLength: ThemeMaxLength, AllowedPattern: themeAllowedPattern, } result := Sanitize(input, opts) // Title case words := strings.Fields(result) for i, word := range words { if len(word) > 0 { runes := []rune(word) runes[0] = unicode.ToUpper(runes[0]) for j := 1; j < len(runes); j++ { runes[j] = unicode.ToLower(runes[j]) } words[i] = string(runes) } } return strings.Join(words, " ") } // RemoveHTMLTags removes all HTML tags from a string. func RemoveHTMLTags(input string) string { return tagRegex.ReplaceAllString(input, "") } // ContainsDangerousPatterns checks if input contains potentially dangerous patterns. func ContainsDangerousPatterns(input string) bool { lowerInput := strings.ToLower(input) for _, pattern := range dangerousPatterns { if strings.Contains(lowerInput, pattern) { return true } } return false } // IsValidEmail performs basic email validation. func IsValidEmail(email string) bool { emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) return emailRegex.MatchString(email) } func clampByRunes(input string, max int) string { if max <= 0 { return input } runes := []rune(input) if len(runes) <= max { return input } return string(runes[:max]) }