Skip to main content
โšก Calmops

Input Validation and Sanitization in Go

Input Validation and Sanitization in Go

Introduction

Input validation and sanitization are critical security practices. Unvalidated input is the root cause of many security vulnerabilities including SQL injection, XSS, command injection, and buffer overflows. This guide teaches you how to properly validate and sanitize input in Go applications.

Core Concepts

Validation vs Sanitization

  • Validation: Checking if input meets expected criteria (type, format, length)
  • Sanitization: Removing or escaping dangerous characters

Common Input Vulnerabilities

  1. SQL Injection: Malicious SQL in database queries
  2. Command Injection: Malicious commands in system calls
  3. XSS: Malicious scripts in web output
  4. Path Traversal: Accessing files outside intended directory
  5. Buffer Overflow: Writing beyond buffer boundaries

Good: Input Validation

Type and Format Validation

package main

import (
	"fmt"
	"regexp"
	"strconv"
	"strings"
)

// โœ… GOOD: Validate email format
func ValidateEmail(email string) error {
	pattern := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`
	matched, _ := regexp.MatchString(pattern, email)
	if !matched {
		return fmt.Errorf("invalid email format")
	}
	return nil
}

// โœ… GOOD: Validate URL
func ValidateURL(url string) error {
	if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
		return fmt.Errorf("invalid URL scheme")
	}
	if len(url) > 2048 {
		return fmt.Errorf("URL too long")
	}
	return nil
}

// โœ… GOOD: Validate integer range
func ValidateAge(age string) (int, error) {
	val, err := strconv.Atoi(age)
	if err != nil {
		return 0, fmt.Errorf("age must be a number")
	}
	if val < 0 || val > 150 {
		return 0, fmt.Errorf("age must be between 0 and 150")
	}
	return val, nil
}

// โœ… GOOD: Validate string length
func ValidateUsername(username string) error {
	if len(username) < 3 {
		return fmt.Errorf("username too short")
	}
	if len(username) > 32 {
		return fmt.Errorf("username too long")
	}
	
	// Only alphanumeric and underscore
	matched, _ := regexp.MatchString(`^[a-zA-Z0-9_]+$`, username)
	if !matched {
		return fmt.Errorf("username contains invalid characters")
	}
	return nil
}

func main() {
	fmt.Println(ValidateEmail("[email protected]"))
	fmt.Println(ValidateAge("25"))
	fmt.Println(ValidateUsername("john_doe"))
}

Whitelist Validation

package main

import (
	"fmt"
)

// โœ… GOOD: Whitelist allowed values
func ValidateSortOrder(order string) error {
	allowed := map[string]bool{
		"asc":  true,
		"desc": true,
	}
	
	if !allowed[order] {
		return fmt.Errorf("invalid sort order: %s", order)
	}
	return nil
}

// โœ… GOOD: Whitelist file extensions
func ValidateFileExtension(filename string) error {
	allowed := map[string]bool{
		".pdf":  true,
		".doc":  true,
		".docx": true,
		".txt":  true,
	}
	
	// Extract extension
	ext := filename[len(filename)-4:]
	if !allowed[ext] {
		return fmt.Errorf("file type not allowed")
	}
	return nil
}

// โœ… GOOD: Whitelist allowed operations
func ValidateOperation(op string) error {
	allowed := []string{"create", "read", "update", "delete"}
	
	for _, a := range allowed {
		if op == a {
			return nil
		}
	}
	return fmt.Errorf("invalid operation: %s", op)
}

func main() {
	fmt.Println(ValidateSortOrder("asc"))
	fmt.Println(ValidateFileExtension("document.pdf"))
	fmt.Println(ValidateOperation("create"))
}

Good: Input Sanitization

SQL Injection Prevention

package main

import (
	"database/sql"
	"fmt"
)

// โœ… GOOD: Use parameterized queries
func GetUserByEmail(db *sql.DB, email string) error {
	// Parameterized query prevents SQL injection
	query := "SELECT id, name FROM users WHERE email = ?"
	row := db.QueryRow(query, email)
	
	var id int
	var name string
	if err := row.Scan(&id, &name); err != nil {
		return err
	}
	
	fmt.Printf("User: %s\n", name)
	return nil
}

// โœ… GOOD: Use prepared statements
func InsertUser(db *sql.DB, name, email string) error {
	stmt, err := db.Prepare("INSERT INTO users (name, email) VALUES (?, ?)")
	if err != nil {
		return err
	}
	defer stmt.Close()
	
	_, err = stmt.Exec(name, email)
	return err
}

// โŒ BAD: String concatenation (vulnerable!)
func BadGetUserByEmail(db *sql.DB, email string) error {
	// NEVER do this!
	query := "SELECT id, name FROM users WHERE email = '" + email + "'"
	row := db.QueryRow(query)
	
	var id int
	var name string
	if err := row.Scan(&id, &name); err != nil {
		return err
	}
	
	fmt.Printf("User: %s\n", name)
	return nil
}

Command Injection Prevention

package main

import (
	"fmt"
	"os/exec"
)

// โœ… GOOD: Use exec.Command with separate arguments
func SafeExecuteCommand(filename string) (string, error) {
	// Arguments are passed separately, preventing injection
	cmd := exec.Command("cat", filename)
	output, err := cmd.Output()
	return string(output), err
}

// โœ… GOOD: Validate filename before use
func SafeReadFile(filename string) (string, error) {
	// Validate filename
	if err := ValidateFilename(filename); err != nil {
		return "", err
	}
	
	cmd := exec.Command("cat", filename)
	output, err := cmd.Output()
	return string(output), err
}

func ValidateFilename(filename string) error {
	// Check for path traversal
	if filename == ".." || filename == "." {
		return fmt.Errorf("invalid filename")
	}
	
	// Check for path separators
	for _, char := range filename {
		if char == '/' || char == '\\' {
			return fmt.Errorf("path separators not allowed")
		}
	}
	
	return nil
}

// โŒ BAD: Shell injection vulnerability
func BadExecuteCommand(filename string) (string, error) {
	// NEVER do this!
	cmd := exec.Command("sh", "-c", "cat "+filename)
	output, err := cmd.Output()
	return string(output), err
}

XSS Prevention

package main

import (
	"fmt"
	"html"
)

// โœ… GOOD: Escape HTML output
func SafeDisplayUserComment(comment string) string {
	// Escape HTML special characters
	return html.EscapeString(comment)
}

// โœ… GOOD: Use template auto-escaping
func SafeRenderTemplate(userInput string) string {
	// Go templates auto-escape by default
	template := fmt.Sprintf("<p>%s</p>", html.EscapeString(userInput))
	return template
}

// โŒ BAD: No escaping (XSS vulnerability)
func BadDisplayUserComment(comment string) string {
	// NEVER do this!
	return fmt.Sprintf("<p>%s</p>", comment)
}

func main() {
	malicious := "<script>alert('XSS')</script>"
	fmt.Println(SafeDisplayUserComment(malicious))
}

Advanced Patterns

Comprehensive Input Validator

package main

import (
	"fmt"
	"regexp"
	"strings"
)

// ValidationRule defines a validation rule
type ValidationRule struct {
	Name      string
	Validator func(string) error
}

// InputValidator validates input against multiple rules
type InputValidator struct {
	rules []ValidationRule
}

func (v *InputValidator) AddRule(rule ValidationRule) {
	v.rules = append(v.rules, rule)
}

func (v *InputValidator) Validate(input string) error {
	for _, rule := range v.rules {
		if err := rule.Validator(input); err != nil {
			return fmt.Errorf("%s: %w", rule.Name, err)
		}
	}
	return nil
}

// Common validators
func NotEmpty(input string) error {
	if strings.TrimSpace(input) == "" {
		return fmt.Errorf("cannot be empty")
	}
	return nil
}

func MaxLength(max int) func(string) error {
	return func(input string) error {
		if len(input) > max {
			return fmt.Errorf("exceeds maximum length of %d", max)
		}
		return nil
	}
}

func MinLength(min int) func(string) error {
	return func(input string) error {
		if len(input) < min {
			return fmt.Errorf("below minimum length of %d", min)
		}
		return nil
	}
}

func MatchPattern(pattern string) func(string) error {
	return func(input string) error {
		matched, _ := regexp.MatchString(pattern, input)
		if !matched {
			return fmt.Errorf("does not match pattern: %s", pattern)
		}
		return nil
	}
}

func main() {
	validator := &InputValidator{}
	validator.AddRule(ValidationRule{
		Name:      "Not empty",
		Validator: NotEmpty,
	})
	validator.AddRule(ValidationRule{
		Name:      "Min length",
		Validator: MinLength(3),
	})
	validator.AddRule(ValidationRule{
		Name:      "Max length",
		Validator: MaxLength(32),
	})
	validator.AddRule(ValidationRule{
		Name:      "Alphanumeric",
		Validator: MatchPattern(`^[a-zA-Z0-9]+$`),
	})
	
	if err := validator.Validate("john123"); err != nil {
		fmt.Printf("Validation error: %v\n", err)
	}
}

Best Practices

1. Validate Early

// โœ… GOOD: Validate at entry point
func HandleRequest(input string) error {
	if err := ValidateInput(input); err != nil {
		return err
	}
	// Process validated input
	return nil
}

// โŒ BAD: Validate late
func BadHandleRequest(input string) error {
	// Process first, validate later
	result := processInput(input)
	if err := ValidateInput(input); err != nil {
		return err
	}
	return nil
}

2. Use Whitelists, Not Blacklists

// โœ… GOOD: Whitelist approach
func IsAllowedCharacter(ch rune) bool {
	allowed := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"
	return strings.ContainsRune(allowed, ch)
}

// โŒ BAD: Blacklist approach
func IsDangerousCharacter(ch rune) bool {
	dangerous := "<>\"'&"
	return strings.ContainsRune(dangerous, ch)
}

3. Fail Securely

// โœ… GOOD: Reject invalid input
func ProcessUserInput(input string) error {
	if err := ValidateInput(input); err != nil {
		return fmt.Errorf("invalid input: %w", err)
	}
	// Process
	return nil
}

// โŒ BAD: Accept invalid input
func BadProcessUserInput(input string) error {
	// Process anyway
	return nil
}

Resources

Summary

Input validation and sanitization are essential security practices. Always validate input early, use whitelists instead of blacklists, and use parameterized queries for database operations. Remember: never trust user input.

Comments