CORS, Security & Rate Limiting

Production-grade HTTP security middleware โ€” CORS, CSRF protection, secure headers, request body limits, gzip compression, and in-memory or Redis-backed rate limiting.

CORS Middleware

Controls which origins can make cross-domain requests to your server:

import "github.com/CodeSyncr/nimbus/middleware"

// Allow a single origin
app.Router.Use(middleware.CORS("https://app.example.com"))

// Allow all origins (development only)
app.Router.Use(middleware.CORS("*"))

Headers Set

HeaderValue
Access-Control-Allow-OriginThe configured origin
Access-Control-Allow-MethodsGET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-HeadersContent-Type, Authorization

Preflight OPTIONS requests are automatically handled with 204 No Content.

Per-Group CORS

// API allows different origin than the main app
api := app.Router.Group("/api")
api.Use(middleware.CORS("https://mobile.myapp.com"))

CORS with Credentials

If your frontend sends cookies cross-origin, you need Access-Control-Allow-Credentials. Write a custom middleware:

func CORSWithCredentials(origin string) router.Middleware {
    return func(next router.HandlerFunc) router.HandlerFunc {
        return func(c *http.Context) error {
            c.Response.Header().Set("Access-Control-Allow-Origin", origin)
            c.Response.Header().Set("Access-Control-Allow-Credentials", "true")
            c.Response.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
            c.Response.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
            if c.Request.Method == "OPTIONS" {
                c.Status(204)
                return nil
            }
            return next(c)
        }
    }
}

Security Headers

The SecureHeaders middleware sets OWASP-recommended HTTP security headers:

// Use production defaults
app.Router.Use(middleware.SecureHeaders(middleware.DefaultSecureHeadersConfig()))

// Or customize
app.Router.Use(middleware.SecureHeaders(middleware.SecureHeadersConfig{
    HSTS:                  "max-age=63072000; includeSubDomains",
    ContentTypeNoSniff:    true,
    FrameOptions:          "DENY",
    XSSProtection:         "1; mode=block",
    ReferrerPolicy:        "strict-origin-when-cross-origin",
    ContentSecurityPolicy: "default-src 'self'; script-src 'self' 'unsafe-inline'",
    PermissionsPolicy:     "camera=(), microphone=(), geolocation=()",
}))
HeaderDefaultPurpose
Strict-Transport-Securitymax-age=63072000; includeSubDomainsForce HTTPS
X-Content-Type-OptionsnosniffPrevent MIME sniffing
X-Frame-OptionsDENYPrevent clickjacking
X-XSS-Protection1; mode=blockXSS filter (legacy)
Referrer-Policystrict-origin-when-cross-originControl referrer leaking
Content-Security-Policy(configurable)Restrict resource loading
Permissions-Policy(configurable)Restrict browser APIs

CSRF Protection

Protects against Cross-Site Request Forgery by requiring a valid token on POST/PUT/PATCH/DELETE:

// Create a token store
csrfStore := middleware.NewMemoryCSRFStore()

// Apply middleware
app.Router.Use(middleware.CSRF(csrfStore))

Including the Token

In .nimbus templates with the Shield plugin, use {{ .csrfField }} which renders a hidden input automatically:

<form method="POST" action="/posts">
    {{ .csrfField }}
    <input type="text" name="title">
    <button type="submit">Create</button>
</form>

In JavaScript SPAs, send the token via header:

fetch('/api/posts', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': token,
    },
    body: JSON.stringify({ title: 'Hello' }),
});

The middleware checks for the token in the _csrf form field or the X-CSRF-Token header.

For multi-instance deployments, implement the CSRFStore interface backed by Redis or your database instead of the in-memory store.


Rate Limiting

In-Memory

// 100 requests per minute, keyed by client IP
app.Router.Use(middleware.RateLimit(100, time.Minute, middleware.DefaultKeyFn))

Redis-Backed (multi-instance)

import "github.com/redis/go-redis/v9"

rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379"})

app.Router.Use(middleware.RateLimitRedis(rdb, 1000, time.Hour, middleware.DefaultKeyFn))

Custom Key Function

// Rate limit by API key
app.Router.Use(middleware.RateLimit(1000, time.Hour, func(r *http.Request) string {
    return r.Header.Get("X-API-Key")
}))

// Strict limit on login
loginLimiter := middleware.RateLimit(5, time.Minute, middleware.DefaultKeyFn)
auth := app.Router.Group("/auth")
auth.Use(loginLimiter)
auth.Post("/login", controllers.Login)

DefaultKeyFn extracts the client IP, handling X-Forwarded-For and X-Real-IP headers for reverse proxy setups.


Body Limit

Prevents denial-of-service by capping request body size:

app.Router.Use(middleware.BodyLimit(10 << 20))  // 10 MB global

// Larger limit for file upload routes
upload := app.Router.Group("/upload")
upload.Use(middleware.BodyLimit(50 << 20))  // 50 MB

Gzip Compression

Transparent gzip compression when the client sends Accept-Encoding: gzip:

app.Router.Use(middleware.Gzip())

Recommended Middleware Stack

A production-ready order (middleware executes top-to-bottom on requests, bottom-to-top on responses):

app := nimbus.New()

// 1. Recovery โ€” catch panics first
app.Router.Use(middleware.Recover())

// 2. Logging โ€” log all requests
app.Router.Use(middleware.Logger())

// 3. Security headers โ€” set on every response
app.Router.Use(middleware.SecureHeaders(middleware.DefaultSecureHeadersConfig()))

// 4. CORS โ€” handle preflight before other middleware
app.Router.Use(middleware.CORS(os.Getenv("CORS_ORIGIN")))

// 5. Body limit โ€” reject oversized requests early
app.Router.Use(middleware.BodyLimit(10 << 20))

// 6. Gzip โ€” compress responses
app.Router.Use(middleware.Gzip())

// 7. Rate limiting โ€” throttle abuse
app.Router.Use(middleware.RateLimit(100, time.Minute, middleware.DefaultKeyFn))

// 8. CSRF โ€” protect forms
app.Router.Use(middleware.CSRF(middleware.NewMemoryCSRFStore()))

Place Recover first so it catches panics from all downstream middleware.