monorepo/native/desktop/maplefile/internal/service/inputvalidation/inputvalidation.go

263 lines
6.4 KiB
Go

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
}