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.

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.
go get github.com/rs/corsc := 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.
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 protect clients on the browser side:
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.
For state mutations in web apps, make sure requests come from your own pages:
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.
Prevent giant requests from exhausting memory:
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.
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.
Don't trust input — validate before using it:
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.
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:
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.
The X-Forwarded-For header can be spoofed by clients. Use middleware.RealIP, which only trusts known proxies:
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.
Key takeaways:
http.MaxBytesReader limits the request body size.middleware.ThrottleBacklog absorbs load spikes.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.