package inputvalidation import ( "fmt" "net/mail" "regexp" "strings" "unicode" "unicode/utf8" ) // Validation limits for input fields const ( // Email limits MaxEmailLength = 254 // RFC 5321 // Name limits (collection names, file names, user names) MinNameLength = 1 MaxNameLength = 255 // Display name limits MaxDisplayNameLength = 100 // Description limits MaxDescriptionLength = 1000 // UUID format (standard UUID v4) UUIDLength = 36 // OTT (One-Time Token) limits OTTLength = 8 // 8-digit code // Password limits MinPasswordLength = 8 MaxPasswordLength = 128 ) // uuidRegex matches standard UUID format (8-4-4-4-12) var uuidRegex = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`) // ottRegex matches 8-digit OTT codes var ottRegex = regexp.MustCompile(`^[0-9]{8}$`) // ValidateEmail validates an email address func ValidateEmail(email string) error { if email == "" { return fmt.Errorf("email is required") } // Check length if len(email) > MaxEmailLength { return fmt.Errorf("email exceeds maximum length of %d characters", MaxEmailLength) } // Use Go's mail package for RFC 5322 validation _, err := mail.ParseAddress(email) if err != nil { return fmt.Errorf("invalid email format") } // Additional checks for security if strings.ContainsAny(email, "\x00\n\r") { return fmt.Errorf("email contains invalid characters") } return nil } // ValidateUUID validates a UUID string func ValidateUUID(id, fieldName string) error { if id == "" { return fmt.Errorf("%s is required", fieldName) } if len(id) != UUIDLength { return fmt.Errorf("%s must be a valid UUID", fieldName) } if !uuidRegex.MatchString(id) { return fmt.Errorf("%s must be a valid UUID format", fieldName) } return nil } // ValidateName validates a name field (collection name, filename, etc.) func ValidateName(name, fieldName string) error { if name == "" { return fmt.Errorf("%s is required", fieldName) } // Check length if len(name) > MaxNameLength { return fmt.Errorf("%s exceeds maximum length of %d characters", fieldName, MaxNameLength) } // Check for valid UTF-8 if !utf8.ValidString(name) { return fmt.Errorf("%s contains invalid characters", fieldName) } // Check for control characters (except tab and newline which might be valid in descriptions) for _, r := range name { if r < 32 && r != '\t' && r != '\n' && r != '\r' { return fmt.Errorf("%s contains invalid control characters", fieldName) } // Also check for null byte and other dangerous characters if r == 0 { return fmt.Errorf("%s contains null characters", fieldName) } } // Check that it's not all whitespace if strings.TrimSpace(name) == "" { return fmt.Errorf("%s cannot be empty or whitespace only", fieldName) } return nil } // ValidateDisplayName validates a display name (first name, last name, etc.) func ValidateDisplayName(name, fieldName string) error { // Display names can be empty (optional fields) if name == "" { return nil } // Check length if len(name) > MaxDisplayNameLength { return fmt.Errorf("%s exceeds maximum length of %d characters", fieldName, MaxDisplayNameLength) } // Check for valid UTF-8 if !utf8.ValidString(name) { return fmt.Errorf("%s contains invalid characters", fieldName) } // Check for control characters for _, r := range name { if r < 32 || !unicode.IsPrint(r) { if r != ' ' { // Allow spaces return fmt.Errorf("%s contains invalid characters", fieldName) } } } return nil } // ValidateDescription validates a description field func ValidateDescription(desc string) error { // Descriptions can be empty (optional) if desc == "" { return nil } // Check length if len(desc) > MaxDescriptionLength { return fmt.Errorf("description exceeds maximum length of %d characters", MaxDescriptionLength) } // Check for valid UTF-8 if !utf8.ValidString(desc) { return fmt.Errorf("description contains invalid characters") } // Check for null bytes if strings.ContainsRune(desc, 0) { return fmt.Errorf("description contains null characters") } return nil } // ValidateOTT validates a one-time token (8-digit code) func ValidateOTT(ott string) error { if ott == "" { return fmt.Errorf("verification code is required") } // Trim whitespace (users might copy-paste with spaces) ott = strings.TrimSpace(ott) if !ottRegex.MatchString(ott) { return fmt.Errorf("verification code must be an 8-digit number") } return nil } // ValidatePassword validates a password func ValidatePassword(password string) error { if password == "" { return fmt.Errorf("password is required") } if len(password) < MinPasswordLength { return fmt.Errorf("password must be at least %d characters", MinPasswordLength) } if len(password) > MaxPasswordLength { return fmt.Errorf("password exceeds maximum length of %d characters", MaxPasswordLength) } // Check for null bytes (could indicate injection attempt) if strings.ContainsRune(password, 0) { return fmt.Errorf("password contains invalid characters") } return nil } // ValidateCollectionID is a convenience function for collection ID validation func ValidateCollectionID(id string) error { return ValidateUUID(id, "collection ID") } // ValidateFileID is a convenience function for file ID validation func ValidateFileID(id string) error { return ValidateUUID(id, "file ID") } // ValidateTagID is a convenience function for tag ID validation func ValidateTagID(id string) error { return ValidateUUID(id, "tag ID") } // ValidateCollectionName validates a collection name func ValidateCollectionName(name string) error { return ValidateName(name, "collection name") } // ValidateFileName validates a file name func ValidateFileName(name string) error { if err := ValidateName(name, "filename"); err != nil { return err } // Additional file-specific validations // Check for path traversal attempts if strings.Contains(name, "..") { return fmt.Errorf("filename cannot contain path traversal sequences") } // Check for path separators if strings.ContainsAny(name, "/\\") { return fmt.Errorf("filename cannot contain path separators") } return nil } // SanitizeString removes or replaces potentially dangerous characters // This is a defense-in-depth measure - validation should be done first func SanitizeString(s string) string { // Remove null bytes s = strings.ReplaceAll(s, "\x00", "") // Trim excessive whitespace s = strings.TrimSpace(s) return s }