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
- SQL Injection: Malicious SQL in database queries
- Command Injection: Malicious commands in system calls
- XSS: Malicious scripts in web output
- Path Traversal: Accessing files outside intended directory
- 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
- OWASP Input Validation: https://owasp.org/www-community/attacks/xss/
- Go Security: https://golang.org/doc/security
- SQL Injection Prevention: https://owasp.org/www-community/attacks/SQL_Injection
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