The Security Middleware Suite: CORS / CSRF / JWT / Rate Limit / Binder
The Security Middleware Suite: CORS / CSRF / JWT / Rate Limit / Binder
Gortex handles body caps, CORS, CSRF, JWT, rate limiting, and log redaction at the framework layer. Keeping those controls together avoids configuring a different security baseline in every service.
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.
Notes
- 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.