Skip to content

CWE-502: Deserialization of Untrusted Data - Go

Overview

Insecure deserialization vulnerabilities in Go applications occur when untrusted data is deserialized without proper validation, allowing attackers to manipulate object state, execute arbitrary code, or cause denial of service. Unlike languages like Java or Python where deserialization gadget chains enable remote code execution, Go's standard serialization formats (encoding/json, encoding/xml, encoding/gob) have more limited attack surfaces due to Go's type system and lack of reflection-based method invocation during deserialization.

However, Go applications remain vulnerable to insecure deserialization in several ways. The encoding/gob package is particularly dangerous when deserializing untrusted data because it can instantiate arbitrary types and populate fields, potentially causing unexpected behavior or resource exhaustion. JSON and XML deserialization can lead to type confusion, integer overflow, excessive memory allocation, or logic flaws when unmarshaling into interface{} types or when application logic makes assumptions about deserialized data structure.

Third-party serialization libraries (MessagePack, Protocol Buffers, YAML) introduce additional risks. YAML deserialization is especially dangerous - libraries like gopkg.in/yaml.v2 can execute arbitrary Go code through YAML tags if not properly configured. Even with safer formats, blindly trusting deserialized data without validation enables attack vectors like privilege escalation (manipulating role fields), authentication bypass (forging session data), or business logic exploitation (modifying price/quantity fields).

Primary Defence: Avoid deserializing untrusted data entirely when possible. Use JSON for data interchange with strict schema validation. Never use encoding/gob or YAML deserialization with untrusted input. Validate all deserialized data before use, checking types, ranges, and business logic constraints.

Common Vulnerable Patterns

gob Deserialization from User Input

// VULNERABLE - gob with untrusted data
package main

import (
    "encoding/gob"
    "io"
    "net/http"
)

type User struct {
    Username string
    IsAdmin  bool
    Balance  int64
}

func deserializeHandler(w http.ResponseWriter, r *http.Request) {
    // DANGEROUS: gob.Decoder on untrusted input
    decoder := gob.NewDecoder(r.Body)

    var user User
    err := decoder.Decode(&user)
    if err != nil {
        http.Error(w, "Decode failed", 400)
        return
    }

    // VULNERABLE: Attacker controls IsAdmin field
    if user.IsAdmin {
        // Grant admin privileges - privilege escalation!
        grantAdminAccess(user.Username)
    }

    // VULNERABLE: Attacker controls Balance
    creditAccount(user.Username, user.Balance)
}

// ATTACK: Attacker sends gob-encoded User{Username: "attacker", IsAdmin: true, Balance: 1000000}
// Result: Privilege escalation and arbitrary balance manipulation

Why this is vulnerable: encoding/gob deserializes binary data into Go structs, but provides no protection against field manipulation. An attacker can craft gob-encoded data with IsAdmin: true and arbitrary Balance values. The application blindly trusts the deserialized data, granting admin privileges and crediting money without server-side verification. Gob is designed for trusted communication between Go programs, not for processing untrusted user input.

JSON Deserialization into interface{}

// VULNERABLE - JSON into interface{} without validation
import (
    "encoding/json"
    "fmt"
    "net/http"
)

func processJSONHandler(w http.ResponseWriter, r *http.Request) {
    var data map[string]interface{}

    // Deserialize user input
    if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
        http.Error(w, "Invalid JSON", 400)
        return
    }

    // VULNERABLE: Type assertion without checking
    userID := data["user_id"].(string)
    role := data["role"].(string)

    // DANGEROUS: Trusting deserialized role
    if role == "admin" {
        grantAdminPrivileges(userID)
    }

    // VULNERABLE: Type confusion attack
    amount := data["amount"].(float64)
    processPayment(userID, int64(amount))
}

// ATTACK 1: {"user_id": "attacker", "role": "admin"}
// Result: Privilege escalation
//
// ATTACK 2: {"user_id": "victim", "amount": 1.7976931348623157e+308}
// Result: Integer overflow when converting float64 to int64
//
// ATTACK 3: {"user_id": "attacker", "amount": "not a number"}
// Result: Panic from type assertion failure

Why this is vulnerable: Deserializing into interface{} types removes type safety. Attackers can provide unexpected types (string instead of number) causing panics, or exploit type conversion issues (extremely large floats overflowing to int64). The application trusts user-controlled "role" field without server-side authorization checks, enabling privilege escalation. Type assertions like .(string) panic on type mismatch, potentially causing denial of service.

YAML Deserialization

// VULNERABLE - YAML with code execution risk
import (
    "io"
    "net/http"

    "gopkg.in/yaml.v2"
)

type Config struct {
    Host     string
    Port     int
    Features map[string]interface{}
}

func uploadConfigHandler(w http.ResponseWriter, r *http.Request) {
    body, _ := io.ReadAll(r.Body)

    var config Config
    // DANGEROUS: YAML deserialization can execute code
    err := yaml.Unmarshal(body, &config)
    if err != nil {
        http.Error(w, "Invalid YAML", 400)
        return
    }

    // Apply configuration
    applyConfig(config)
}

// ATTACK: YAML with malicious tags
// !!python/object/apply:os.system ["rm -rf /"]
// (Note: Go's yaml.v2 doesn't execute Python, but can cause DoS
// and yaml.v3 has other risks with custom unmarshalers)

Why this is vulnerable: YAML is a complex format with features like anchors, aliases, and custom tags that can lead to denial of service through "billion laughs" attacks (exponential expansion) or hash collision attacks. While gopkg.in/yaml.v2 doesn't have the same code execution risks as PyYAML, it can still cause resource exhaustion. YAML's flexibility makes it unsuitable for untrusted input. Switching to yaml.v3 with custom unmarshaling can reintroduce code execution risks if not carefully implemented.

Session/Cookie Deserialization

// VULNERABLE - Deserializing session data without validation
import (
    "encoding/base64"
    "encoding/json"
    "net/http"
)

type Session struct {
    UserID   string
    Username string
    IsAdmin  bool
    Expiry   int64
}

func getSession(r *http.Request) (*Session, error) {
    cookie, err := r.Cookie("session")
    if err != nil {
        return nil, err
    }

    // Decode base64
    data, err := base64.StdEncoding.DecodeString(cookie.Value)
    if err != nil {
        return nil, err
    }

    // VULNERABLE: Deserialize without signature verification
    var session Session
    if err := json.Unmarshal(data, &session); err != nil {
        return nil, err
    }

    // DANGEROUS: Trust deserialized IsAdmin field
    return &session, nil
}

func adminHandler(w http.ResponseWriter, r *http.Request) {
    session, _ := getSession(r)

    // VULNERABLE: Client-controlled IsAdmin value
    if session.IsAdmin {
        // Attacker granted admin access!
        w.Write([]byte("Admin panel"))
    }
}

// ATTACK: Attacker modifies cookie, sets IsAdmin=true, base64 encodes
// Result: Complete authentication bypass

Why this is vulnerable: Base64 encoding is not encryption - it provides no integrity protection. Attackers can decode the session cookie, modify the JSON (changing IsAdmin to true), re-encode it, and send it back. The application blindly trusts the deserialized session data without verifying its authenticity. Session data must be either stored server-side (referenced by a random token) or cryptographically signed/encrypted to prevent tampering.

Secure Patterns

Use JSON with Strong Type Validation

// SECURE - JSON with strict type checking and validation
package main

import (
    "encoding/json"
    "errors"
    "fmt"
    "io"
    "net/http"
)

type UserRequest struct {
    Username string `json:"username"`
    Email    string `json:"email"`
    Age      int    `json:"age"`
}

func (ur *UserRequest) Validate() error {
    // Validate username
    if len(ur.Username) < 3 || len(ur.Username) > 32 {
        return errors.New("username must be 3-32 characters")
    }

    // Validate email format
    if !isValidEmail(ur.Email) {
        return errors.New("invalid email format")
    }

    // Validate age range
    if ur.Age < 0 || ur.Age > 150 {
        return errors.New("age must be between 0 and 150")
    }

    return nil
}

func createUserHandler(w http.ResponseWriter, r *http.Request) {
    // Limit request size
    r.Body = http.MaxBytesReader(w, r.Body, 1048576) // 1MB

    var userReq UserRequest

    // SECURE: Decode into strongly-typed struct
    decoder := json.NewDecoder(r.Body)
    decoder.DisallowUnknownFields() // Reject unexpected fields

    if err := decoder.Decode(&userReq); err != nil {
        http.Error(w, "Invalid request format", http.StatusBadRequest)
        return
    }

    // SECURE: Validate all fields before use
    if err := userReq.Validate(); err != nil {
        http.Error(w, fmt.Sprintf("Validation failed: %v", err), http.StatusBadRequest)
        return
    }

    // Server-side authorization - never trust client
    // IsAdmin determined by backend logic, not user input
    isAdmin := checkAdminPermissions(r.Context())

    // Create user with validated data
    user := createUser(userReq.Username, userReq.Email, userReq.Age, isAdmin)

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]interface{}{
        "status": "created",
        "user":   user,
    })
}

func isValidEmail(email string) bool {
    // Simplified - use proper email validation library
    return len(email) > 3 && contains(email, "@")
}

func contains(s, substr string) bool {
    return len(s) >= len(substr) && 
           (s == substr || len(s) > len(substr) && 
           (s[:len(substr)] == substr || contains(s[1:], substr)))
}

Why this works: Deserializing into strongly-typed structs eliminates many type confusion attacks - JSON decoder will reject mismatched types. DisallowUnknownFields() prevents attackers from injecting unexpected fields that might be processed elsewhere. The Validate() method checks all constraints (length, format, range) before the data is used. Critically, security-sensitive fields like IsAdmin are determined server-side through checkAdminPermissions(), never from user input. http.MaxBytesReader prevents resource exhaustion from extremely large payloads.

Signed Session Tokens with HMAC

// SECURE - Cryptographically signed session data
import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "encoding/json"
    "errors"
    "net/http"
    "time"
)

var sessionSecret = []byte("your-256-bit-secret-key-here-change-this") // From env var in production

type SessionData struct {
    UserID   string `json:"user_id"`
    Username string `json:"username"`
    Expiry   int64  `json:"expiry"`
}

type SignedSession struct {
    Data      string `json:"data"`
    Signature string `json:"signature"`
}

func createSignedSession(userID, username string) (string, error) {
    // Create session data
    session := SessionData{
        UserID:   userID,
        Username: username,
        Expiry:   time.Now().Add(24 * time.Hour).Unix(),
    }

    // Serialize to JSON
    sessionJSON, err := json.Marshal(session)
    if err != nil {
        return "", err
    }

    // Base64 encode
    dataB64 := base64.StdEncoding.EncodeToString(sessionJSON)

    // SECURE: Generate HMAC signature
    mac := hmac.New(sha256.New, sessionSecret)
    mac.Write([]byte(dataB64))
    signature := base64.StdEncoding.EncodeToString(mac.Sum(nil))

    // Combine data and signature
    signed := SignedSession{
        Data:      dataB64,
        Signature: signature,
    }

    signedJSON, err := json.Marshal(signed)
    if err != nil {
        return "", err
    }

    return base64.StdEncoding.EncodeToString(signedJSON), nil
}

func verifyAndDecodeSession(cookieValue string) (*SessionData, error) {
    // Decode outer base64
    signedJSON, err := base64.StdEncoding.DecodeString(cookieValue)
    if err != nil {
        return nil, errors.New("invalid session format")
    }

    // Unmarshal signed session
    var signed SignedSession
    if err := json.Unmarshal(signedJSON, &signed); err != nil {
        return nil, errors.New("invalid session structure")
    }

    // SECURE: Verify HMAC signature
    mac := hmac.New(sha256.New, sessionSecret)
    mac.Write([]byte(signed.Data))
    expectedMAC := mac.Sum(nil)

    providedMAC, err := base64.StdEncoding.DecodeString(signed.Signature)
    if err != nil {
        return nil, errors.New("invalid signature format")
    }

    if !hmac.Equal(expectedMAC, providedMAC) {
        return nil, errors.New("signature verification failed")
    }

    // Decode data
    sessionJSON, err := base64.StdEncoding.DecodeString(signed.Data)
    if err != nil {
        return nil, errors.New("invalid session data")
    }

    // Unmarshal session data
    var session SessionData
    if err := json.Unmarshal(sessionJSON, &session); err != nil {
        return nil, errors.New("invalid session content")
    }

    // SECURE: Verify expiry
    if time.Now().Unix() > session.Expiry {
        return nil, errors.New("session expired")
    }

    return &session, nil
}

func protectedHandler(w http.ResponseWriter, r *http.Request) {
    cookie, err := r.Cookie("session")
    if err != nil {
        http.Error(w, "Not authenticated", http.StatusUnauthorized)
        return
    }

    session, err := verifyAndDecodeSession(cookie.Value)
    if err != nil {
        http.Error(w, "Invalid session", http.StatusUnauthorized)
        return
    }

    // Use verified session data
    fmt.Fprintf(w, "Welcome, %s!", session.Username)
}

Why this works: HMAC-SHA256 signatures ensure session data integrity - any tampering invalidates the signature. The signature is verified using constant-time comparison (hmac.Equal) to prevent timing attacks. Even if attackers decode and modify the session data, they cannot forge a valid signature without the secret key. Expiry checks prevent replay attacks with old sessions. The secret key must be stored securely (environment variable, secrets manager) and rotated periodically. This approach allows stateless sessions while preventing tampering.

Avoid gob - Use JSON for Data Exchange

// SECURE - Use JSON instead of gob for untrusted data
import (
    "encoding/json"
    "net/http"
)

type SafeUserData struct {
    Username string `json:"username"`
    Email    string `json:"email"`
}

func safeDeserializeHandler(w http.ResponseWriter, r *http.Request) {
    var userData SafeUserData

    // SECURE: JSON with type safety
    decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1048576))
    decoder.DisallowUnknownFields()

    if err := decoder.Decode(&userData); err != nil {
        http.Error(w, "Invalid input", http.StatusBadRequest)
        return
    }

    // Validate
    if len(userData.Username) == 0 || len(userData.Email) == 0 {
        http.Error(w, "Missing required fields", http.StatusBadRequest)
        return
    }

    // Authorization determined server-side, NEVER from user input
    isAdmin := checkUserRole(r.Context(), userData.Username)

    processUser(userData.Username, userData.Email, isAdmin)

    w.WriteHeader(http.StatusOK)
}

Why this works: JSON is simpler and safer than gob for untrusted input. It doesn't support arbitrary type instantiation or complex object graphs that could be exploited. The strongly-typed SafeUserData struct ensures only expected fields are accepted. DisallowUnknownFields() rejects payloads with extra fields an attacker might inject hoping they'll be processed elsewhere. Security-critical decisions (admin status) are made server-side based on database lookups or authentication context, never from deserialized user data.

Server-Side Session Storage

// SECURE - Server-side session storage with random tokens
import (
    "crypto/rand"
    "encoding/base64"
    "errors"
    "io"
    "net/http"
    "sync"
    "time"
)

type ServerSession struct {
    UserID    string
    Username  string
    IsAdmin   bool
    CreatedAt time.Time
}

var (
    sessionStore = make(map[string]*ServerSession)
    sessionMutex sync.RWMutex
)

func generateSessionToken() (string, error) {
    // SECURE: Cryptographically random token
    bytes := make([]byte, 32)
    if _, err := io.ReadFull(rand.Reader, bytes); err != nil {
        return "", err
    }
    return base64.URLEncoding.EncodeToString(bytes), nil
}

func createSession(userID, username string, isAdmin bool) (string, error) {
    token, err := generateSessionToken()
    if err != nil {
        return "", err
    }

    session := &ServerSession{
        UserID:    userID,
        Username:  username,
        IsAdmin:   isAdmin,
        CreatedAt: time.Now(),
    }

    sessionMutex.Lock()
    sessionStore[token] = session
    sessionMutex.Unlock()

    return token, nil
}

func getSessionByToken(token string) (*ServerSession, error) {
    sessionMutex.RLock()
    session, exists := sessionStore[token]
    sessionMutex.RUnlock()

    if !exists {
        return nil, errors.New("session not found")
    }

    // Check expiry
    if time.Since(session.CreatedAt) > 24*time.Hour {
        // Clean up expired session
        sessionMutex.Lock()
        delete(sessionStore, token)
        sessionMutex.Unlock()
        return nil, errors.New("session expired")
    }

    return session, nil
}

func loginHandler(w http.ResponseWriter, r *http.Request) {
    username := r.FormValue("username")
    password := r.FormValue("password")

    // Authenticate user
    userID, isAdmin, err := authenticateUser(username, password)
    if err != nil {
        http.Error(w, "Invalid credentials", http.StatusUnauthorized)
        return
    }

    // Create server-side session
    token, err := createSession(userID, username, isAdmin)
    if err != nil {
        http.Error(w, "Session creation failed", http.StatusInternalServerError)
        return
    }

    // Set cookie with random token only
    http.SetCookie(w, &http.Cookie{
        Name:     "session_token",
        Value:    token,
        Path:     "/",
        MaxAge:   86400, // 24 hours
        HttpOnly: true,
        Secure:   true,
        SameSite: http.SameSiteStrictMode,
    })

    w.WriteHeader(http.StatusOK)
}

func authenticatedHandler(w http.ResponseWriter, r *http.Request) {
    cookie, err := r.Cookie("session_token")
    if err != nil {
        http.Error(w, "Not authenticated", http.StatusUnauthorized)
        return
    }

    session, err := getSessionByToken(cookie.Value)
    if err != nil {
        http.Error(w, "Invalid session", http.StatusUnauthorized)
        return
    }

    // SECURE: Session data comes from server, not client
    if session.IsAdmin {
        w.Write([]byte("Admin panel"))
    } else {
        w.Write([]byte("User dashboard"))
    }
}

func authenticateUser(username, password string) (string, bool, error) {
    // Database lookup, password verification
    // Returns userID, isAdmin status, error
    return "", false, nil
}

Why this works: Session data never leaves the server, eliminating deserialization risks entirely. The client only receives a cryptographically random token that cannot be predicted or manipulated. All session data (including IsAdmin status) is stored server-side, making it impossible for attackers to tamper with. Token-to-session lookup is protected by mutex for thread safety. Expired sessions are automatically cleaned up. For production, replace the in-memory map with Redis, Memcached, or a database for distributed systems and persistence.

Schema Validation with JSON Schema

// SECURE - JSON Schema validation for complex data
import (
    "encoding/json"
    "net/http"

    "github.com/xeipuuv/gojsonschema"
)

var orderSchema = `{
    "type": "object",
    "required": ["customer_id", "items", "total"],
    "properties": {
        "customer_id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 50
        },
        "items": {
            "type": "array",
            "minItems": 1,
            "maxItems": 100,
            "items": {
                "type": "object",
                "required": ["product_id", "quantity", "price"],
                "properties": {
                    "product_id": {"type": "string"},
                    "quantity": {"type": "integer", "minimum": 1, "maximum": 1000},
                    "price": {"type": "number", "minimum": 0}
                }
            }
        },
        "total": {
            "type": "number",
            "minimum": 0
        }
    },
    "additionalProperties": false
}`

func validateOrder(orderJSON []byte) error {
    schemaLoader := gojsonschema.NewStringLoader(orderSchema)
    documentLoader := gojsonschema.NewBytesLoader(orderJSON)

    result, err := gojsonschema.Validate(schemaLoader, documentLoader)
    if err != nil {
        return err
    }

    if !result.Valid() {
        // Collect all validation errors
        var errMsg string
        for _, err := range result.Errors() {
            errMsg += err.String() + "; "
        }
        return errors.New(errMsg)
    }

    return nil
}

func submitOrderHandler(w http.ResponseWriter, r *http.Request) {
    body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 1048576))
    if err != nil {
        http.Error(w, "Request too large", http.StatusRequestEntityTooLarge)
        return
    }

    // SECURE: Validate against schema before deserializing
    if err := validateOrder(body); err != nil {
        http.Error(w, "Invalid order format: "+err.Error(), http.StatusBadRequest)
        return
    }

    // Deserialize validated data
    var order map[string]interface{}
    json.Unmarshal(body, &order)

    // Additional business logic validation
    if !verifyOrderTotal(order) {
        http.Error(w, "Order total mismatch", http.StatusBadRequest)
        return
    }

    // Process order
    processOrder(order)

    w.WriteHeader(http.StatusCreated)
}

func verifyOrderTotal(order map[string]interface{}) bool {
    // Recalculate total server-side, don't trust client
    items := order["items"].([]interface{})
    var calculatedTotal float64

    for _, item := range items {
        itemMap := item.(map[string]interface{})
        quantity := itemMap["quantity"].(float64)
        price := itemMap["price"].(float64)
        calculatedTotal += quantity * price
    }

    providedTotal := order["total"].(float64)

    // Allow small floating point differences
    return abs(calculatedTotal-providedTotal) < 0.01
}

func abs(x float64) float64 {
    if x < 0 {
        return -x
    }
    return x
}

Why this works: JSON Schema provides comprehensive validation before deserialization - checking types, required fields, array lengths, number ranges, and string patterns. "additionalProperties": false prevents injection of unexpected fields. Schema validation occurs before any processing, rejecting invalid data early. Critically, business logic validation (verifyOrderTotal) recalculates the total server-side rather than trusting the client-provided value, preventing price manipulation attacks. This defense-in-depth approach combines schema validation with business rule enforcement.

Framework-Specific Guidance

Gin with Request Binding and Validation

// SECURE - Gin with built-in validation
package main

import (
    "net/http"

    "github.com/gin-gonic/gin"
    "github.com/gin-gonic/gin/binding"
    "github.com/go-playground/validator/v10"
)

type CreateProductRequest struct {
    Name        string  `json:"name" binding:"required,min=3,max=100"`
    Description string  `json:"description" binding:"max=500"`
    Price       float64 `json:"price" binding:"required,gt=0,lte=1000000"`
    Stock       int     `json:"stock" binding:"required,gte=0,lte=10000"`
    Category    string  `json:"category" binding:"required,oneof=electronics clothing food"`
}

func main() {
    r := gin.Default()

    // Register custom validators if needed
    if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
        v.RegisterValidation("notadmin", func(fl validator.FieldLevel) bool {
            return fl.Field().String() != "admin"
        })
    }

    r.POST("/products", createProductHandler)

    r.Run(":8080")
}

func createProductHandler(c *gin.Context) {
    var req CreateProductRequest

    // SECURE: Gin validates based on struct tags
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": "Validation failed",
            "details": err.Error(),
        })
        return
    }

    // Additional custom validation
    if req.Price*float64(req.Stock) > 10000000 {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": "Total inventory value exceeds limit",
        })
        return
    }

    // Server-side authorization
    userID := c.GetString("user_id") // From auth middleware
    if !canCreateProduct(userID) {
        c.JSON(http.StatusForbidden, gin.H{
            "error": "Insufficient permissions",
        })
        return
    }

    // Create product with validated data
    product := createProduct(req.Name, req.Description, req.Price, req.Stock, req.Category)

    c.JSON(http.StatusCreated, gin.H{
        "status": "created",
        "product": product,
    })
}

func canCreateProduct(userID string) bool {
    // Database lookup for permissions
    return true
}

func createProduct(name, desc string, price float64, stock int, category string) interface{} {
    return map[string]interface{}{"name": name}
}

Why this works: Gin's ShouldBindJSON automatically validates based on struct field tags: required, min, max, gt (greater than), lte (less than or equal), oneof for enums. Validation happens during deserialization, rejecting invalid data before it reaches handler logic. Custom validators can be registered for complex rules. The framework handles validation error responses automatically. Authorization is still performed server-side through canCreateProduct() lookup, never trusting client input for permissions.

Echo with Custom Validators

// SECURE - Echo with validator integration
package main

import (
    "net/http"

    "github.com/go-playground/validator/v10"
    "github.com/labstack/echo/v4"
)

type CustomValidator struct {
    validator *validator.Validate
}

func (cv *CustomValidator) Validate(i interface{}) error {
    if err := cv.validator.Struct(i); err != nil {
        return err
    }
    return nil
}

type UpdateUserRequest struct {
    Email    string `json:"email" validate:"required,email"`
    Age      int    `json:"age" validate:"required,gte=18,lte=120"`
    Username string `json:"username" validate:"required,min=3,max=32,alphanum"`
}

func main() {
    e := echo.New()

    // Register validator
    e.Validator = &CustomValidator{validator: validator.New()}

    e.POST("/users/:id", updateUserHandler)

    e.Start(":8080")
}

func updateUserHandler(c echo.Context) error {
    userID := c.Param("id")

    var req UpdateUserRequest

    // Bind and validate
    if err := c.Bind(&req); err != nil {
        return c.JSON(http.StatusBadRequest, map[string]string{
            "error": "Invalid request format",
        })
    }

    // SECURE: Validate with registered validator
    if err := c.Validate(&req); err != nil {
        return c.JSON(http.StatusBadRequest, map[string]string{
            "error": "Validation failed: " + err.Error(),
        })
    }

    // Server-side authorization
    currentUserID := c.Get("user_id").(string) // From JWT middleware
    if currentUserID != userID && !isAdmin(currentUserID) {
        return c.JSON(http.StatusForbidden, map[string]string{
            "error": "Cannot update other users",
        })
    }

    // Update user
    updateUser(userID, req.Email, req.Age, req.Username)

    return c.JSON(http.StatusOK, map[string]string{
        "status": "updated",
    })
}

func isAdmin(userID string) bool {
    // Check database for admin role
    return false
}

func updateUser(id, email string, age int, username string) {}

Why this works: Echo integrates with go-playground/validator for comprehensive validation. The CustomValidator wrapper allows using struct tags for validation rules. Email validation, age ranges, alphanumeric checks, and length constraints are enforced before the handler processes data. Authorization logic checks if the current user can update the target user, preventing unauthorized modifications. Validation errors are caught early and returned with clear error messages.

Additional Resources