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
- CWE-502: Deserialization of Untrusted Data
- Go encoding/gob Package (avoid for untrusted input)
- Go encoding/json Package
- Gorilla Sessions (server-side session management)
- OWASP Deserialization Cheat Sheet
- go-playground/validator
- gojsonschema Library