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:

  1. secret ≥ 32 bytes: NewJWTService rejects any secret shorter than MinJWTSecretBytes (32, matching the SHA-256 output length) with ErrJWTSecretTooShort, and config.Validate enforces it at config-load time. A short key weakens the HMAC.
  2. HS256 only: on verify, keyFuncHS256 checks token.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.MaxBytesReader to 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 typ claim splitting access / refresh, and legacy no-typ tokens rejected.
  • Rate limiting emits X-RateLimit-* / Retry-After; but per-IP limiting needs TrustedProxies or a forged X-Forwarded-For slips past it.
  • Logs redact sensitive request-body fields; LogResponseBody is 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.