// Package validation provides input validation utilities for security package validation import ( "fmt" "path/filepath" "regexp" "strings" ) // ValidationRule is a function that validates a string value type ValidationRule func(value string) error // Validator provides reusable validation rules type Validator struct { errors []string } // NewValidator creates a new validator func NewValidator() *Validator { return &Validator{errors: make([]string, 0)} } // Add adds a field to validate with the given rules func (v *Validator) Add(name, value string, rules ...ValidationRule) { for _, rule := range rules { if err := rule(value); err != nil { v.errors = append(v.errors, fmt.Sprintf("%s: %v", name, err)) } } } // Valid returns nil if validation passed, otherwise returns an error func (v *Validator) Valid() error { if len(v.errors) > 0 { return fmt.Errorf("validation failed: %s", strings.Join(v.errors, "; ")) } return nil } // Common validation rules // SafeName validates alphanumeric + underscore + hyphen only var SafeName ValidationRule = func(v string) error { if matched, _ := regexp.MatchString(`^[a-zA-Z0-9_-]+$`, v); !matched { return fmt.Errorf("must contain only alphanumeric characters, underscores, and hyphens") } return nil } // MaxLength validates maximum string length func MaxLength(max int) ValidationRule { return func(v string) error { if len(v) > max { return fmt.Errorf("exceeds maximum length of %d", max) } return nil } } // MinLength validates minimum string length func MinLength(min int) ValidationRule { return func(v string) error { if len(v) < min { return fmt.Errorf("must be at least %d characters", min) } return nil } } // NoPathTraversal validates no path traversal sequences var NoPathTraversal ValidationRule = func(v string) error { if strings.Contains(v, "..") || strings.Contains(v, "../") || strings.Contains(v, "..\\") { return fmt.Errorf("path traversal sequence detected") } return nil } // NoShellMetacharacters validates no shell metacharacters var NoShellMetacharacters ValidationRule = func(v string) error { dangerous := []string{";", "|", "&", "`", "$", "(", ")", "<", ">", "*", "?"} for _, char := range dangerous { if strings.Contains(v, char) { return fmt.Errorf("shell metacharacter '%s' detected", char) } } return nil } // NoNullBytes validates no null bytes var NoNullBytes ValidationRule = func(v string) error { if strings.Contains(v, "\x00") { return fmt.Errorf("null byte detected") } return nil } // ValidPath validates a path is within a base directory func ValidPath(basePath string) ValidationRule { return func(v string) error { cleaned := filepath.Clean(v) absPath, err := filepath.Abs(cleaned) if err != nil { return fmt.Errorf("invalid path: %w", err) } absBase, err := filepath.Abs(basePath) if err != nil { return fmt.Errorf("invalid base path: %w", err) } if !strings.HasPrefix(absPath, absBase) { return fmt.Errorf("path escapes base directory") } return nil } } // MatchesPattern validates against a regex pattern func MatchesPattern(pattern, description string) ValidationRule { re := regexp.MustCompile(pattern) return func(v string) error { if !re.MatchString(v) { return fmt.Errorf("must match pattern: %s", description) } return nil } } // Whitelist validates against a whitelist of allowed values func Whitelist(allowed ...string) ValidationRule { return func(v string) error { for _, a := range allowed { if v == a { return nil } } return fmt.Errorf("value not in whitelist") } } // Sanitize removes dangerous characters from input func Sanitize(input string) string { // Remove null bytes input = strings.ReplaceAll(input, "\x00", "") // Remove control characters input = strings.ReplaceAll(input, "\r", "") return input } // ValidateJobName validates a job name is safe func ValidateJobName(jobName string) error { validator := NewValidator() validator.Add("job_name", jobName, MinLength(1), MaxLength(64), SafeName, NoPathTraversal, NoShellMetacharacters, ) return validator.Valid() } // ValidateExperimentID validates an experiment ID is safe func ValidateExperimentID(id string) error { validator := NewValidator() validator.Add("experiment_id", id, MinLength(1), MaxLength(128), SafeName, NoPathTraversal, ) return validator.Valid() } // ValidateCommand validates a command string is safe func ValidateCommand(cmd string) error { validator := NewValidator() validator.Add("command", cmd, MinLength(1), MaxLength(1024), NoShellMetacharacters, ) return validator.Valid() }