Echo Input Validation
Introduction
Input validation is a critical aspect of web application security. When your application receives data from external sources (such as user inputs, API requests, or file uploads), it's essential to validate this data before processing it to prevent security vulnerabilities like SQL injection, cross-site scripting (XSS), and command injection.
In the Echo framework, implementing proper input validation helps ensure that only properly formatted and expected data is processed by your application. This guide will walk you through techniques to validate user inputs effectively when building applications with Echo.
Why Input Validation Matters
Before diving into implementation, let's understand why input validation is crucial:
- Protection against injection attacks: Validating inputs helps prevent SQL injection, command injection, and other injection attacks.
- Data integrity: Ensures that the data stored in your database meets quality standards.
- Application stability: Prevents unexpected errors or crashes due to malformed inputs.
- User experience: Provides immediate feedback to users when they submit invalid data.
Basic Input Validation in Echo
Echo provides several ways to handle and validate user inputs. Let's start with basic validation techniques.
Request Binding and Validation
Echo offers built-in functionality to bind request data to Go structs and validate them using tags.
import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/go-playground/validator/v10"
)
// User represents user data structure
type User struct {
Name string `json:"name" validate:"required,min=2,max=50"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"required,gte=18,lte=120"`
Password string `json:"password" validate:"required,min=8"`
}
// Custom validator
type CustomValidator struct {
validator *validator.Validate
}
func (cv *CustomValidator) Validate(i interface{}) error {
return cv.validator.Struct(i)
}
func main() {
e := echo.New()
e.Validator = &CustomValidator{validator: validator.New()}
e.POST("/users", func(c echo.Context) error {
u := new(User)
// Bind and validate request data
if err := c.Bind(u); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid request payload")
}
if err := c.Validate(u); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
// Process valid user data
return c.JSON(http.StatusCreated, u)
})
e.Logger.Fatal(e.Start(":8080"))
}
In this example:
- We define a
User
struct with validation tags. - We create a custom validator that uses the
go-playground/validator
package. - The validation occurs when calling
c.Validate(u)
.
Validation Tags
The go-playground/validator
package supports numerous validation tags:
required
: Field must be presentemail
: Must be a valid email addressmin=x
: Minimum length (for strings) or value (for numbers)max=x
: Maximum length (for strings) or value (for numbers)gte=x
: Greater than or equal tolte=x
: Less than or equal tooneof=x y z
: Value must be one of the options
Advanced Input Validation Techniques
Custom Validation Functions
You can create custom validation functions for complex validation scenarios:
func main() {
e := echo.New()
v := validator.New()
// Register custom validation
v.RegisterValidation("nospecialchars", noSpecialChars)
e.Validator = &CustomValidator{validator: v}
// Routes
e.POST("/users", createUser)
e.Logger.Fatal(e.Start(":8080"))
}
// Custom validation function
func noSpecialChars(fl validator.FieldLevel) bool {
value := fl.Field().String()
return !strings.ContainsAny(value, "!@#$%^&*()_+{}|:\"<>?/\\")
}
// User struct with custom validation
type User struct {
Username string `json:"username" validate:"required,min=4,max=20,nospecialchars"`
// other fields
}
Handling File Upload Validation
When handling file uploads, it's important to validate file types, sizes, and contents:
e.POST("/upload", func(c echo.Context) error {
// Read file from request
file, err := c.FormFile("document")
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid file")
}
// Validate file size
if file.Size > 5*1024*1024 { // 5MB limit
return echo.NewHTTPError(http.StatusBadRequest, "File too large (max 5MB)")
}
// Validate file extension
filename := file.Filename
ext := strings.ToLower(filepath.Ext(filename))
validExts := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".pdf": true}
if !validExts[ext] {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid file type")
}
// Process valid file
// ...
return c.JSON(http.StatusOK, map[string]string{"status": "File uploaded successfully"})
})
Protecting Against Common Security Issues
Cross-Site Scripting (XSS) Protection
Always sanitize user inputs that will be displayed in HTML to prevent XSS attacks:
import (
"github.com/microcosm-cc/bluemonday"
"github.com/labstack/echo/v4"
)
func main() {
e := echo.New()
e.POST("/comments", func(c echo.Context) error {
comment := struct {
Content string `json:"content"`
}{}
if err := c.Bind(&comment); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid request")
}
// Sanitize HTML content
p := bluemonday.UGCPolicy() // Use a policy appropriate for your application
sanitizedContent := p.Sanitize(comment.Content)
// Store sanitized content
// ...
return c.JSON(http.StatusOK, map[string]string{
"content": sanitizedContent,
})
})
e.Logger.Fatal(e.Start(":8080"))
}
SQL Injection Prevention
When working with databases, use parameterized queries or ORM libraries rather than concatenating user inputs directly into SQL queries:
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
func getUserProfile(c echo.Context) error {
userId := c.QueryParam("id")
// Bad practice - vulnerable to SQL injection
// query := "SELECT * FROM users WHERE id = " + userId
// Good practice - parameterized query
db, _ := sql.Open("mysql", "user:password@/dbname")
defer db.Close()
// Using parameterized query
var name, email string
err := db.QueryRow("SELECT name, email FROM users WHERE id = ?", userId).Scan(&name, &email)
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, "User not found")
}
return c.JSON(http.StatusOK, map[string]string{
"name": name,
"email": email,
})
}
Real-World Example: User Registration API
Let's create a more complete example of a user registration API with comprehensive validation:
package main
import (
"net/http"
"regexp"
"strings"
"github.com/labstack/echo/v4"
"github.com/go-playground/validator/v10"
"golang.org/x/crypto/bcrypt"
)
// User represents registration data
type User struct {
Username string `json:"username" validate:"required,alphanum,min=4,max=20"`
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required,min=8,containsany=!@#$%^&*()_+{}|:\"<>?/\\,containsany=0123456789,containsany=ABCDEFGHIJKLMNOPQRSTUVWXYZ"`
FullName string `json:"full_name" validate:"required"`
Country string `json:"country" validate:"required,iso3166_1_alpha2"`
Terms bool `json:"terms_accepted" validate:"required,eq=true"`
}
// CustomValidator integrates the validator package
type CustomValidator struct {
validator *validator.Validate
}
// Validate implements echo.Validator interface
func (cv *CustomValidator) Validate(i interface{}) error {
return cv.validator.Struct(i)
}
// Custom password strength validation
func validatePasswordStrength(fl validator.FieldLevel) bool {
password := fl.Field().String()
// Check for at least one uppercase, one lowercase, one number, and one special character
hasUpper := regexp.MustCompile(`[A-Z]`).MatchString(password)
hasLower := regexp.MustCompile(`[a-z]`).MatchString(password)
hasNumber := regexp.MustCompile(`[0-9]`).MatchString(password)
hasSpecial := regexp.MustCompile(`[!@#$%^&*()\-_=+{};:,<.>]`).MatchString(password)
return hasUpper && hasLower && hasNumber && hasSpecial
}
func main() {
e := echo.New()
// Setup validator
v := validator.New()
v.RegisterValidation("strongpassword", validatePasswordStrength)
e.Validator = &CustomValidator{validator: v}
// Routes
e.POST("/register", registerUser)
e.Logger.Fatal(e.Start(":8080"))
}
func registerUser(c echo.Context) error {
user := new(User)
// Bind data from the request
if err := c.Bind(user); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid request payload")
}
// Validate the user data
if err := c.Validate(user); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
// Additional custom validation
user.Email = strings.TrimSpace(strings.ToLower(user.Email))
user.Username = strings.TrimSpace(user.Username)
// Check if user already exists (in a real app, this would query a database)
// ...
// Hash password for storage
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Password processing failed")
}
// Store user in database (omitted for brevity)
// ...
// Create response without returning the password
response := map[string]interface{}{
"username": user.Username,
"email": user.Email,
"status": "registered",
}
return c.JSON(http.StatusCreated, response)
}
Best Practices for Input Validation in Echo
-
Validate on both client and server sides: Client-side validation improves user experience, while server-side validation is essential for security.
-
Use appropriate validation rules: Choose validation rules that match your specific requirements.
-
Normalize data before validation: Convert inputs to consistent formats (e.g., trimming whitespace, converting to lowercase) before validation.
-
Fail securely: When validation fails, provide generic error messages to users to avoid giving away too much information.
-
Implement rate limiting: Protect against brute force attacks by implementing rate limiting on form submissions.
-
Log validation failures: Monitor validation failures to detect potential attack patterns.
-
Use contextual validation: Some fields may have different validation rules based on context or other fields' values.
-
Avoid blacklisting: Prefer whitelisting (accepting only known-good inputs) over blacklisting (blocking known-bad inputs).
Summary
Input validation is a crucial security measure for any web application. In Echo, you can leverage the built-in binding capabilities along with validation packages like go-playground/validator
to create robust, secure applications.
By implementing proper input validation, you protect your application from various attacks, ensure data quality, and provide a better experience for your users. Remember that validation is just one layer of defense – it should be combined with other security measures like proper authentication, authorization, and output encoding.
Additional Resources
- Echo Framework Documentation
- go-playground/validator Documentation
- OWASP Input Validation Cheat Sheet
Exercises
-
Create an API endpoint that validates a product submission with fields for name, price, description, and category.
-
Implement a form validation system for a contact form with fields for name, email, subject, and message.
-
Build an advanced validation system that validates a user profile update with conditional validation (e.g., if a user wants to change their password, they must provide both old and new passwords).
-
Create custom validators for specific data formats like phone numbers, postal codes, or credit card numbers.
-
Implement a file upload endpoint that validates file types, sizes, and contents before storing them.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)