Frontend Labs
⋯

Day 17 — encoding/json

Learning Objectives

  • Use streaming JSON decoding to avoid buffering large request bodies
  • Write custom marshalers for sensitive fields (mask passwords in logs)
  • Handle unknown fields for security — json.Decoder.DisallowUnknownFields()

Key Concepts

// Struct tags control JSON field names and behavior
type AuthRequest struct {
    Username string `json:"username"`
    Password string `json:"password,omitempty"` // omit if empty
    MFA      string `json:"mfa_code,omitempty"`
}

// Decoding — use decoder, not Unmarshal, for streaming
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()  // reject {"username":"a", "admin":true}
if err := dec.Decode(&req); err != nil {
    http.Error(w, "invalid request", 400)
    return
}

// Custom marshaling — hide sensitive fields
type LogSafeAuthRequest struct {
    Username string `json:"username"`
    Password string `json:"-"` // never marshaled
}

// json.RawMessage — defer parsing of a field
type Event struct {
    Type    string          `json:"type"`
    Payload json.RawMessage `json:"payload"` // parse later based on Type
}

Exercise: Secure JSON API Pipeline

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "strings"
)

// LoginRequest — incoming from client
type LoginRequest struct {
    Username string `json:"username"`
    Password string `json:"password"`
    TOTPCode string `json:"totp_code,omitempty"`
}

// Validate enforces field rules
func (r *LoginRequest) Validate() error {
    if r.Username == "" {
        return fmt.Errorf("username required")
    }
    if len(r.Username) > 64 {
        return fmt.Errorf("username too long (max 64)")
    }
    if r.Password == "" {
        return fmt.Errorf("password required")
    }
    if len(r.Password) < 8 {
        return fmt.Errorf("password too short (min 8)")
    }
    return nil
}

// LoginResponse — sent to client
type LoginResponse struct {
    Token     string `json:"token"`
    ExpiresIn int    `json:"expires_in"` // seconds
}

// AuditEntry — logged (password masked)
type AuditEntry struct {
    Username  string `json:"username"`
    Password  string `json:"-"` // NEVER log
    Action    string `json:"action"`
    Success   bool   `json:"success"`
    Reason    string `json:"reason,omitempty"`
}

func processLogin(body string) {
    dec := json.NewDecoder(strings.NewReader(body))
    dec.DisallowUnknownFields() // OWASP A03: reject parameter pollution

    var req LoginRequest
    if err := dec.Decode(&req); err != nil {
        fmt.Printf("decode error: %v\n", err)
        return
    }

    if err := req.Validate(); err != nil {
        fmt.Printf("validation error: %v\n", err)
        return
    }

    // Audit log — password field is omitted via json:"-"
    audit := AuditEntry{
        Username: req.Username,
        Action:   "login",
        Success:  true,
    }
    auditJSON, _ := json.Marshal(audit)
    fmt.Printf("AUDIT: %s\n", auditJSON)

    // Response
    resp := LoginResponse{Token: "eyJ...", ExpiresIn: 3600}
    var buf bytes.Buffer
    json.NewEncoder(&buf).Encode(resp)
    fmt.Printf("RESPONSE: %s", buf.String())
}

func main() {
    fmt.Println("=== Valid request ===")
    processLogin(`{"username":"alice","password":"s3cr3tP@ss"}`)

    fmt.Println("=== Parameter pollution ===")
    processLogin(`{"username":"alice","password":"s3cr3tP@ss","admin":true}`)

    fmt.Println("=== Missing password ===")
    processLogin(`{"username":"alice"}`)
}

Real-World Context

DisallowUnknownFields() blocks parameter pollution — an attacker adding "admin": true to gain elevated access. The json:"-" tag prevents passwords from appearing in logs. This is OWASP A02 (Cryptographic Failures) — even a logging mistake can expose credentials.


Generated from week3.md · 8 Oct 2026 16:36