Security
6 minute read
The validation package includes built-in security features to protect sensitive data and prevent various attacks through validation.
Sensitive Data Redaction
Protect sensitive data in error messages with redactors:
redactor := func(path string) bool {
return strings.Contains(path, "password") ||
strings.Contains(path, "token") ||
strings.Contains(path, "secret") ||
strings.Contains(path, "api_key")
}
validator := validation.MustNew(
validation.WithRedactor(redactor),
)
How Redaction Works
When a field is redacted, its value in error messages is replaced with [REDACTED]:
type User struct {
Email string `validate:"required,email"`
Password string `validate:"required,min=8"`
}
user := User{
Email: "invalid",
Password: "secret123",
}
err := validator.Validate(ctx, &user)
// Error: email: must be valid email (value: "invalid")
// Error: password: too short (value: "[REDACTED]")
Pattern-Based Redaction
func sensitiveFieldRedactor(path string) bool {
sensitive := []string{
"password",
"token",
"secret",
"api_key",
"credit_card",
"ssn",
"private_key",
}
pathLower := strings.ToLower(path)
for _, keyword := range sensitive {
if strings.Contains(pathLower, keyword) {
return true
}
}
return false
}
validator := validation.MustNew(
validation.WithRedactor(sensitiveFieldRedactor),
)
Path-Specific Redaction
func pathRedactor(path string) bool {
redactedPaths := map[string]bool{
"user.password": true,
"user.security_question": true,
"payment.card_number": true,
"payment.cvv": true,
"auth.refresh_token": true,
}
return redactedPaths[path]
}
Redacting Nested Fields
type Payment struct {
CardNumber string `json:"card_number" validate:"required,credit_card"`
CVV string `json:"cvv" validate:"required,len=3"`
}
type Order struct {
Payment Payment `json:"payment"`
}
redactor := func(path string) bool {
// Redact payment.card_number and payment.cvv
return strings.HasPrefix(path, "payment.card_number") ||
strings.HasPrefix(path, "payment.cvv")
}
Security Limits
Maximum Nesting Depth
The package protects against stack overflow from deeply nested structures:
// Built-in protection: max depth = 100 levels
const maxRecursionDepth = 100
This applies to:
ComputePresence()- Presence map computation- Nested struct validation
- Recursive data structures
Maximum Fields
Limit fields processed in partial validation:
validator := validation.MustNew(
validation.WithMaxFields(5000), // Default: 10000
)
Prevents attacks with extremely large JSON objects:
{
"field1": "value",
"field2": "value",
// ... 100,000 more fields
}
Maximum Errors
Limit errors returned to prevent memory exhaustion:
validator := validation.MustNew(
validation.WithMaxErrors(100), // Default: unlimited
)
When limit is reached, Truncated flag is set:
var verr *validation.Error
if errors.As(err, &verr) {
if verr.Truncated {
log.Warn("more validation errors exist (truncated)")
}
}
Schema Cache Size
Limit JSON Schema cache to prevent memory exhaustion:
validator := validation.MustNew(
validation.WithMaxCachedSchemas(2048), // Default: 1024
)
Uses LRU eviction - oldest schemas are removed when limit is reached.
Input Validation Security
Prevent Injection Attacks
Always validate input format before using in queries or commands:
type SearchRequest struct {
Query string `validate:"required,max=100,alphanum"`
}
// Safe from SQL injection (alphanumeric only)
err := validator.Validate(ctx, &req)
Sanitize HTML
import "html"
type CreatePostRequest struct {
Title string `validate:"required,max=200"`
Content string `validate:"required,max=10000"`
}
func (r *CreatePostRequest) Validate() error {
// Sanitize HTML
r.Title = html.EscapeString(r.Title)
r.Content = html.EscapeString(r.Content)
return nil
}
Validate File Paths
import "path/filepath"
type UploadRequest struct {
Filename string `validate:"required"`
}
func (r *UploadRequest) Validate() error {
// Prevent path traversal attacks
cleaned := filepath.Clean(r.Filename)
if strings.Contains(cleaned, "..") {
return errors.New("invalid filename: path traversal detected")
}
r.Filename = cleaned
return nil
}
Rate Limiting
Combine validation with rate limiting to prevent abuse:
import "golang.org/x/time/rate"
var limiter = rate.NewLimiter(rate.Every(time.Second), 10)
func ValidateWithRateLimit(ctx context.Context, req any) error {
// Check rate limit first (fast)
if !limiter.Allow() {
return errors.New("rate limit exceeded")
}
// Then validate (slower)
return validation.Validate(ctx, req)
}
Denial of Service Prevention
Request Size Limits
func Handler(w http.ResponseWriter, r *http.Request) {
// Limit request body size
r.Body = http.MaxBytesReader(w, r.Body, 1*1024*1024) // 1MB max
rawJSON, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "request too large", http.StatusRequestEntityTooLarge)
return
}
// Continue with validation
}
Array/Slice Limits
type BatchRequest struct {
Items []Item `validate:"required,min=1,max=100"`
}
// Prevents DoS with extremely large arrays
String Length Limits
type Request struct {
Description string `validate:"max=10000"`
}
// Prevents memory exhaustion from huge strings
Validation Timeout
import "context"
func ValidateWithTimeout(req any) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return validation.Validate(ctx, req)
}
Security Best Practices
1. Always Validate User Input
// Good
func CreateUser(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
json.NewDecoder(r.Body).Decode(&req)
// ALWAYS validate
if err := validation.Validate(r.Context(), &req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Safe to use req
}
// Bad
func CreateUser(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
json.NewDecoder(r.Body).Decode(&req)
// Using unvalidated input - DANGEROUS!
db.Exec("INSERT INTO users VALUES (?, ?)", req.Username, req.Email)
}
2. Validate Before Database Operations
func UpdateUser(ctx context.Context, req UpdateUserRequest) error {
// Validate first
if err := validation.Validate(ctx, &req); err != nil {
return err
}
// Then update database
return db.UpdateUser(ctx, req)
}
3. Use Strict Mode for APIs
validator := validation.MustNew(
validation.WithDisallowUnknownFields(true),
)
// Rejects requests with unexpected fields (typo detection)
4. Redact All Sensitive Fields
func comprehensiveRedactor(path string) bool {
pathLower := strings.ToLower(path)
// Passwords and secrets
if strings.Contains(pathLower, "password") ||
strings.Contains(pathLower, "secret") ||
strings.Contains(pathLower, "token") ||
strings.Contains(pathLower, "key") {
return true
}
// Payment information
if strings.Contains(pathLower, "card") ||
strings.Contains(pathLower, "cvv") ||
strings.Contains(pathLower, "credit") {
return true
}
// Personal information
if strings.Contains(pathLower, "ssn") ||
strings.Contains(pathLower, "social_security") ||
strings.Contains(pathLower, "tax_id") {
return true
}
return false
}
5. Log Validation Failures
err := validation.Validate(ctx, &req)
if err != nil {
var verr *validation.Error
if errors.As(err, &verr) {
// Log validation failures for security monitoring
log.With(
"error_count", len(verr.Fields),
"fields", getFieldPaths(verr.Fields),
"ip", getClientIP(r),
).Warn("validation failed")
}
return err
}
6. Fail Secure
// Good - fail if validation library has issues
validator, err := validation.New(options...)
if err != nil {
panic("failed to create validator: " + err.Error())
}
// Bad - continue without validation
validator, err := validation.New(options...)
if err != nil {
log.Warn("validator creation failed, continuing anyway") // DANGEROUS
}
Common Security Vulnerabilities
SQL Injection
// VULNERABLE
type SearchRequest struct {
Query string // No validation
}
db.Exec("SELECT * FROM users WHERE name = '" + req.Query + "'")
// SAFE
type SearchRequest struct {
Query string `validate:"required,max=100,alphanum"`
}
if err := validation.Validate(ctx, &req); err != nil {
return err
}
db.Exec("SELECT * FROM users WHERE name = ?", req.Query)
Path Traversal
// VULNERABLE
type FileRequest struct {
Path string // No validation
}
os.ReadFile(req.Path) // Could be "../../etc/passwd"
// SAFE
type FileRequest struct {
Path string `validate:"required,max=255"`
}
func (r *FileRequest) Validate() error {
cleaned := filepath.Clean(r.Path)
if strings.Contains(cleaned, "..") {
return errors.New("path traversal detected")
}
if !strings.HasPrefix(cleaned, "/safe/directory/") {
return errors.New("path outside allowed directory")
}
return nil
}
XXE (XML External Entity)
// VULNERABLE
xml.Unmarshal(req.Body, &data)
// SAFE
decoder := xml.NewDecoder(req.Body)
decoder.Strict = true
decoder.Entity = xml.HTMLEntity // Prevent external entities
err := decoder.Decode(&data)
Mass Assignment
// VULNERABLE
type UpdateUserRequest struct {
Email string
Role string // User shouldn't be able to set this!
}
// SAFE - separate request types
type UpdateUserRequest struct {
Email string `validate:"required,email"`
}
type AdminUpdateUserRequest struct {
Email string `validate:"required,email"`
Role string `validate:"required,oneof=user admin"`
}
Security Checklist
- All user input is validated before use
- Sensitive fields are redacted in errors
- Request size limits are enforced
- Array/slice lengths are limited
- Nesting depth is limited (handled automatically)
- Unknown fields are rejected in strict mode
- Validation failures are logged
- Rate limiting is implemented
- Timeouts are set for validation
- SQL queries use parameterized statements
- File paths are sanitized
- HTML is escaped before rendering
- Mass assignment is prevented
Next Steps
- Examples - Complete security examples
- Options Reference - Security-related options
- Error Handling - Handle errors securely
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.