The Security Middleware Suite: CORS / CSRF / JWT / Rate Limit / Binder
The Security Middleware Suite: CORS / CSRF / JWT / Rate Limit / Binder
Security isn’t one middleware, it’s a suite. Gortex consolidates body caps, CORS, CSRF, JWT, rate limiting, and log redaction at the framework layer so business code needn’t reconfigure each time. This post takes them one by one.
Prerequisite: A1 net/http and Middleware.
Binder: body caps
The most direct untrusted input is the request body. The Binder caps a JSON body at 1 MiB (DefaultMaxJSONBodyBytes = 1 << 20) and enforces it with http.MaxBytesReader as it reads — an oversized body fails mid-read rather than being buffered into memory first:
const DefaultMaxJSONBodyBytes int64 = 1 << 20 // 1 MiB
limited := http.MaxBytesReader(c.Response(), originalBody, limit)
// decode `limited`; a body over the limit fails here instead of OOMing
Multipart has its own cap (32 MiB per the framework docs). It’s the most basic move there is — bound untrusted input at the boundary — and it shuts down the oversize-body DoS.
CORS / CSRF
CORS: CORSHandlerWithConfig sets Access-Control-Allow-Origin / -Methods / -Credentials and friends from a CORSConfig, and adds Vary. Note the default AllowOrigins is ["*"] — handy in development, but tighten it to a whitelist for production. Changing that default once at the framework layer for every service is exactly B1’s dependency consolidation.
CSRF: a double-submit token. A random token (32 bytes by default) goes in a cookie, and the client must echo it back in a header; a mismatch is ErrCSRFTokenMismatch, a missing one is ErrCSRFTokenMissing, both 403. The cookie’s SameSite defaults to Lax, and a Skipper can bypass safe routes.
JWT: 32-byte, HS256, typ
The JWT layer has three deliberate hard rules:
- secret ≥ 32 bytes:
NewJWTServicerejects any secret shorter thanMinJWTSecretBytes(32, matching the SHA-256 output length) withErrJWTSecretTooShort, andconfig.Validateenforces it at config-load time. A short key weakens the HMAC. - HS256 only: on verify,
keyFuncHS256checkstoken.Method == jwt.SigningMethodHS256, rejecting even HS384/HS512.
func (s *JWTService) keyFuncHS256(token *jwt.Token) (any, error) {
if token.Method != jwt.SigningMethodHS256 {
return nil, ErrUnexpectedSigningMethod // blocks the algorithm-confusion attack
}
return s.secret, nil
}
Why it matters: asserting on *jwt.SigningMethodHMAC alone would also accept other HMAC algorithms — the classic algorithm-confusion vulnerability. Pinning HS256 is what makes it safe.
3. typ claim: tokens carry a typ (access / refresh). Passing a refresh token where an access token is expected returns ErrInvalidTokenType, and legacy tokens with no typ are rejected outright.
Rate limiting and TrustedProxies
The rate limiter is a per-IP token bucket with a background goroutine pruning expired entries, and it emits X-RateLimit-Limit / -Remaining / -Reset, plus Retry-After when a request is throttled.
But per-IP limiting is only as trustworthy as the IP. A client can simply set a fake X-Forwarded-For to change its apparent IP and slip past the limit. Hence TrustedProxies (a set of CIDRs): X-Forwarded-For / X-Real-IP are honoured only for requests from a trusted proxy; otherwise the forwarding headers are ignored and the real RemoteAddr is used. Without it, per-IP limiting is theatre.
Log redaction
Logs are where passwords and tokens accidentally end up. Gortex’s logger uses a BodyRedactor to mask sensitive fields in the request body (defaulting to DefaultBodyRedactor), and treats headers and query strings the same way.
There’s a limitation worth stating plainly: LogResponseBody is a no-op — the source comment says it’s deprecated and has no effect; the framework doesn’t support logging response bodies. 4xx / 5xx are logged at Warn / Error using the real status code from B4’s tracked responseWriter. Being clear about what is and isn’t logged beats pretending to do everything.
Takeaways
- The Binder uses
http.MaxBytesReaderto cap a JSON body at 1 MiB (multipart has its own cap), shutting down oversize-body DoS. - CORS allows configurable origins (default
*, tighten for production); CSRF uses a double-submit token (32 bytes, SameSite Lax by default), with missing / mismatched → 403. - JWT: secret ≥ 32 bytes, HS256-only (blocks algorithm confusion), a
typclaim splitting access / refresh, and legacy no-typ tokens rejected. - Rate limiting emits
X-RateLimit-*/Retry-After; but per-IP limiting needsTrustedProxiesor a forgedX-Forwarded-Forslips past it. - Logs redact sensitive request-body fields;
LogResponseBodyis a no-op, and 4xx / 5xx use the tracked writer’s real status at Warn / Error.
Source: yshengliao/gortex.
Outline by Sheng, drafted with Claude · Go 1.25 (gortex go.mod) · compiled retroactively · part of the 2026-06-13 blog renovation, paint still drying.