Rate Limiting Bypass in Chi
How Rate Limiting Bypass Manifests in Chi
Rate limiting bypass in Chi-based APIs typically exploits the framework's middleware chain and token parsing mechanisms. Chi's lightweight design, while excellent for performance, can create subtle vulnerabilities when rate limiting isn't properly implemented across all request paths.
The most common bypass pattern occurs in Chi's route matching system. When you define routes with path parameters like /users/:id, Chi uses a prefix tree to match requests. If rate limiting middleware is attached only to specific route handlers rather than the entire router, attackers can exploit unmatched routes to bypass limits entirely.
// Vulnerable: Rate limiting only on /api/* routes
r := chi.NewRouter()
r.Use(rateLimitMiddleware) // Only applies to routes below
r.Get("/api/users/{id}", getUserHandler)
r.Get("/users/{id}", getUserHandler) // No rate limiting!
This creates a bypass where requests to /users/123 aren't rate limited while /api/users/123 are. The fix requires applying rate limiting at the router level or ensuring all routes go through the same middleware chain.
Another Chi-specific bypass vector involves the chi.URLParam function. If your rate limiting logic depends on URL parameters but doesn't properly validate them, attackers can manipulate parameter parsing to create multiple rate limiting contexts. For example, treating /users/123 and /users//123 as different users when they should be the same.
// Vulnerable: Different rate limits for same user
func rateLimitMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID := chi.URLParam(r, "id")
// Missing normalization: /users/123 vs /users//123
key := fmt.Sprintf("rate_limit:%s", userID)
// ... rate limiting logic
next.ServeHTTP(w, r)
})
}
Header-based bypasses are also prevalent in Chi applications. Since Chi doesn't automatically normalize headers, requests with different header case variations (Content-Type vs content-type vs CONTENT-TYPE) might be treated as distinct requests by downstream rate limiting logic.
// Vulnerable: Case-sensitive header handling
func getUserHandler(w http.ResponseWriter, r *http.Request) {
contentType := r.Header.Get("Content-Type")
// Different limits for: application/json vs APPLICATION/JSON
if contentType == "application/json" {
// ... logic
}
}
The framework's context handling can also introduce bypasses. Chi's context.WithValue stores arbitrary data that, if used for rate limiting decisions without proper validation, can be manipulated through context injection in middleware chains.
Chi-Specific Detection
Detecting rate limiting bypasses in Chi requires both static analysis and runtime testing. The most effective approach combines code review with automated scanning to identify both obvious and subtle bypass vectors.
Start with middleware chain analysis. In Chi, the order and scope of middleware application is critical. Use static analysis tools to verify that rate limiting middleware is applied at the correct level:
# Check middleware application in Chi apps
grep -r "Use(" . --include="*.go" | grep -v "//" | head -20
Look for patterns where middleware is applied to specific routes rather than the entire router. The vulnerable pattern shows middleware after route registration, while the secure pattern shows it before:
- r := chi.NewRouter()
- r.Get("/api/*", apiHandler)
- r.Use(rateLimitMiddleware) // Applied too late!
+ r := chi.NewRouter()
+ r.Use(rateLimitMiddleware) // Applied to all routes
+ r.Get("/api/*", apiHandler)
Runtime detection with middleBrick specifically tests Chi's routing behavior by sending requests to similar but distinct paths to identify inconsistent rate limiting. The scanner probes for:
- Path parameter normalization issues (
/users/123vs/users//123) - Case sensitivity in headers and parameters
- Middleware scope boundaries
- Context manipulation possibilities
middleBrick's black-box scanning approach is particularly effective for Chi applications because it doesn't require access to source code. The scanner sends carefully crafted requests to test rate limiting consistency across the API surface.
# Scan a Chi API for rate limiting bypasses
middlebrick scan https://api.example.com --output json
The scanner reports findings with severity levels and specific remediation guidance. For Chi applications, it identifies whether rate limiting is properly applied at the router level versus individual routes.
Manual testing should include fuzzing path parameters and headers to verify consistent rate limiting behavior. Tools like hey or vegeta can help stress test the API while monitoring for inconsistent responses.
# Test rate limiting consistency
for i in {1..100}; do
curl -s -o /dev/null -w "%{http_code}" \
"https://api.example.com/users/123?page=$i" &
done
Chi-Specific Remediation
Fixing rate limiting bypasses in Chi requires a systematic approach that addresses both the framework's specific behaviors and general security principles. The most robust solution combines proper middleware placement with consistent input normalization.
First, ensure rate limiting middleware is applied at the router level, not individual routes. This guarantees consistent coverage across all API endpoints:
package main
import (
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-redis/redis/v8"
"github.com/ulule/limiter/v3"
"github.com/ulule/limiter/v3/drivers/middleware"
"github.com/ulule/limiter/v3/drivers/store/redis"
)
func setupRateLimiting() middleware.Middleware {
// Centralized rate limiting configuration
redisClient := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
store, err := redis.NewStore(redisClient)
if err != nil {
panic(err)
}
rate := limiter.Rate{
Limit: 100, // 100 requests
Period: time.Minute,
}
limiterInstance := limiter.New(store, rate, limiter.WithTrustForwardHeader(true))
return middleware.NewMiddleware(limiterInstance)
}
func main() {
r := chi.NewRouter()
// Apply rate limiting to ALL routes
r.Use(setupRateLimiting())
// Now all routes are protected
r.Get("/api/users/{id}", getUserHandler)
r.Post("/api/users", createUserHandler)
r.Get("/health", healthHandler)
http.ListenAndServe(":3000", r)
}
Second, implement consistent input normalization for all parameters that affect rate limiting decisions. This includes URL parameters, headers, and query strings:
func normalizeRateLimitKey(r *http.Request) string {
// Normalize URL path (remove duplicate slashes)
path := r.URL.Path
normalizedPath := strings.ReplaceAll(path, "//", "/")
// Normalize query parameters (sort and deduplicate)
q := r.URL.Query()
var keys []string
for k := range q {
keys = append(keys, k)
}
sort.Strings(keys)
var normalizedQuery string
for _, k := range keys {
values := q[k]
sort.Strings(values)
normalizedQuery += fmt.Sprintf("%s=%s&", k, strings.Join(values, ","))
}
if normalizedQuery != "" {
normalizedQuery = "?" + normalizedQuery[:len(normalizedQuery)-1]
}
return fmt.Sprintf("%s%s", normalizedPath, normalizedQuery)
}
Third, use Chi's built-in context handling consistently for rate limiting decisions. Store normalized identifiers in the request context and use them throughout your middleware chain:
func rateLimitMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Get normalized identifier from context or create it
ctx := r.Context()
userID := chi.URLParam(r, "id")
// Store in context for downstream use
ctx = context.WithValue(ctx, "rate_limit_key", userID)
// Apply rate limiting using the normalized key
key := fmt.Sprintf("rate_limit:%s", userID)
// ... rate limiting logic
next.ServeHTTP(w, r.WithContext(ctx))
})
}
For distributed systems, implement Redis-based rate limiting to ensure consistency across multiple API instances:
func distributedRateLimit(next http.Handler, limiter *limiter.Limiter) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
context := r.Context()
key := getRateLimitKey(r) // Your normalization function
limit, err := limiter.Get(context, key)
if err != nil {
http.Error(w, "rate limit error", http.StatusInternalServerError)
return
}
if limit.Reached {
http.Error(w, "rate limit exceeded", http.StatusTooManyRequests)
return
}
// Set rate limit headers
w.Header().Set("X-RateLimit-Limit", fmt.Sprintf("%d", limit.Limit))
w.Header().Set("X-RateLimit-Remaining", fmt.Sprintf("%d", limit.Remaining))
w.Header().Set("X-RateLimit-Reset", fmt.Sprintf("%d", limit.Reset.Unix()))
next.ServeHTTP(w, r)
})
}
Finally, implement comprehensive testing to verify that rate limiting is consistent across all routes and input variations. Use table-driven tests to cover edge cases:
func TestRateLimitingConsistency(t *testing.T) {
testCases := []struct {
name string
path string
expected int
}{
{"normal path", "/users/123", 100},
{"duplicate slashes", "/users//123", 100},
{"case sensitivity", "/USERS/123", 100},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest("GET", tc.path, nil)
rr := httptest.NewRecorder()
// Call your handler
handler(rr, req)
// Verify rate limit headers are consistent
limit := rr.Header().Get("X-RateLimit-Limit")
if limit != "100" {
t.Errorf("Expected limit 100, got %s", limit)
}
})
}
}
Related CWEs: resourceConsumption
| CWE ID | Name | Severity |
|---|---|---|
| CWE-400 | Uncontrolled Resource Consumption | HIGH |
| CWE-770 | Allocation of Resources Without Limits | MEDIUM |
| CWE-799 | Improper Control of Interaction Frequency | MEDIUM |
| CWE-835 | Infinite Loop | HIGH |
| CWE-1050 | Excessive Platform Resource Consumption | MEDIUM |