Learn Chi - Security Middleware & Input Hardening
Series/Learn Chi/Episode 14
Episode 14 of 23

Learn Chi - Security Middleware & Input Hardening

This episode hardens your application's defenses: CORS with rs/cors, CSP and HSTS security headers, CSRF protection, body limits, and rate limiting. You will also learn input validation and sanitization, injection and SSRF prevention, and how to safely handle RealIP and X-Forwarded-For.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

Authentication is only half the battle. Episode 14 closes the other side: security middleware and input hardening. This is where your application learns to reject harmful requests at the front door — not just when a user isn't logged in. Topics include CORS for browser access, security headers, CSRF protection, body size limits, rate limiting, input validation, and preventing injection and SSRF. After this episode, your application doesn't just work — it's also hard to break into.

CORS with rs/cors

Configuring Origins

Install rs/cors
go get github.com/rs/cors
Enable CORS
c := cors.New(cors.Options{
    AllowedOrigins: []string{"https://app.example.com"},
    AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
    AllowedHeaders: []string{"Content-Type", "Authorization"},
    MaxAge:         86400,
})
 
r.Use(c.Handler)

cors.New(cors.Options{...}) restricts which origins, methods, and headers are allowed. c.Handler becomes a chi middleware — mounted with r.Use like any other middleware.

Don't Open Every Origin

AllowedOrigins: []string{"*"} is convenient for development but dangerous in production. Always register known origins, or read them from an environment variable. Combine it with middleware.RealIP when the server sits behind a proxy.

Security Headers

Security headers protect clients on the browser side:

Security headers middleware
func securityHeaders(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
        w.Header().Set("Content-Security-Policy",
            "default-src 'self'")
        w.Header().Set("Strict-Transport-Security",
            "max-age=31536000; includeSubDomains")
        w.Header().Set("X-Content-Type-Options", "nosniff")
        w.Header().Set("X-Frame-Options", "DENY")
        w.Header().Set("Referrer-Policy", "no-referrer")
        next.ServeHTTP(w, req)
    })
}

w.Header().Set("Content-Security-Policy", "default-src 'self'") controls which content sources the browser may load. HSTS (Strict-Transport-Security) forces HTTPS for all subsequent visits. Four values you must remember: Content-Security-Policy reduces XSS, Strict-Transport-Security forces HTTPS, X-Frame-Options: DENY prevents clickjacking, and nosniff stops the browser from guessing MIME types.

CSRF and Body Limits

CSRF Protection

For state mutations in web apps, make sure requests come from your own pages:

Validate origin for mutations
if req.Method == http.MethodPost ||
    req.Method == http.MethodPut ||
    req.Method == http.MethodDelete {
    origin := req.Header.Get("Origin")
    if origin != "" && origin != appOrigin {
        http.Error(w, "origin tidak dikenali",
            http.StatusForbidden)
        return
    }
}

The Origin check is a basic CSRF layer for JSON APIs. For classic forms, consider a per-session CSRF token. req.Header.Get("Origin") tells you where the request came from.

Limit Body Size

Prevent giant requests from exhausting memory:

Body limit middleware
func limitBody(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
        req.Body = http.MaxBytesReader(w, req.Body, 1<<20)
        next.ServeHTTP(w, req)
    })
}

http.MaxBytesReader(w, req.Body, 1<<20) caps the body at 1 MiB. When a handler reads beyond the limit, the operation fails with http: request body too large.

Rate Limiting

Limiting Request Frequency

Rate limiting
r.Use(middleware.ThrottleBacklog(
    100,   // konteks aktif maksimal
    1000,  // backlog maksimal
    30*time.Second,
))

middleware.ThrottleBacklog(100, 1000, 30*time.Second) rejects requests when 100 are active and the 1000-slot backlog has been full for 30 seconds. For per-minute IP-based limits, pair it with Redis as in episode 19.

Input Validation and Injection Prevention

Centralized Validation

Don't trust input — validate before using it:

Input validation
func validateUser(u User) error {
    if u.Name == "" {
        return NewAPIError(http.StatusBadRequest, "nama wajib diisi")
    }
    if !validEmail(u.Email) {
        return NewAPIError(http.StatusBadRequest, "email tidak valid")
    }
    return nil
}

NewAPIError(http.StatusBadRequest, ...) rejects input early with a clear message — better to reject at the edge than to have problems in the database.

Preventing SQL Injection and SSRF

SQL injection is already prevented by parameterized queries ($1, $2) as in episode 9. For SSRF, never let users control the URLs your server requests:

SSRF prevention
allowedHosts := map[string]bool{
    "api.internal.example.com": true,
}
 
host := chi.URLParam(req, "host")
if !allowedHosts[host] {
    http.Error(w, "host tidak diizinkan",
        http.StatusForbidden)
    return
}

allowedHosts[host] restricts which hosts the server may reach. Always validate against an allowlist — SSRF often comes from URLs built from user input without checking.

RealIP and X-Forwarded-For

Don't Trust Headers Directly

The X-Forwarded-For header can be spoofed by clients. Use middleware.RealIP, which only trusts known proxies:

Install RealIP
r.Use(middleware.RealIP)

middleware.RealIP takes the real IP from proxy headers and overwrites it into req.RemoteAddr. Without this middleware, logs and rate limiting use a spoofable IP — a common gap in apps behind a proxy. Install RealIP before other middleware that needs the IP, such as logging and rate limiting.

Conclusion

Key takeaways:

  • CORS with rs/cors: register specific origins, not wildcards.
  • Security headers: CSP, HSTS, X-Frame-Options, and nosniff.
  • Basic CSRF protection by checking the Origin header.
  • http.MaxBytesReader limits the request body size.
  • middleware.ThrottleBacklog absorbs load spikes.
  • Validate input at the edge; allowlist hosts to prevent SSRF.
  • middleware.RealIP prevents X-Forwarded-For spoofing.

In the next episode 15 we secure transport: HTTPS, reverse proxies, and advanced networking — manual TLS, Let's Encrypt with autocert and ACME, HTTP/2, reverse proxies on subrouters, and integration with nginx and Caddy.

Learn Chi - Security Middleware & Input Hardening | Learn Chi