Double Free in Chi with Jwt Tokens
Double Free in Chi with Jwt Tokens — how this specific combination creates or exposes the vulnerability
A Double Free in Chi involving JWT tokens typically occurs when the application parses, validates, and then conditionally deallocates or reuses token-related structures more than once during a single request lifecycle. In Chi, this often manifests when JWT parsing middleware runs, attaches claims to the request context, and later, an error-handling or logging routine processes the same token object again without ensuring safe state transitions.
Consider a route that uses jwt middleware to validate a bearer token. If the middleware stores parsed claims in a context value and later an interceptor or finalizer also attempts to free or modify those claims—especially when an early error response is generated—both the Chi router and the developer-supplied handler may attempt to release the same memory. Because Chi’s HTTP handler composition can result in multiple control paths touching the same object, this double deallocation can corrupt the runtime heap or lead to use-after-free behavior when one path still holds a reference.
JWT tokens themselves do not cause Double Free, but the lifecycle management of their parsed representations—claims maps, signing key references, and expiration metadata—does. For example, if you parse a token into a map[string]interface{} and store it in the context, and then a recovery middleware tries to clean up context values on panic, you may end up freeing the same map twice: once by the middleware and once by the framework’s context teardown. This is particularly risky when using interface-heavy token representations that hide underlying pointer semantics.
Real-world patterns that exacerbate this include:
- Multiple middleware layers each inspecting and copying JWT claims without shared ownership semantics.
- Deferred functions that call cleanup helpers on request-scoped objects that may already have been released by the framework.
- Using raw token strings in logging or error reporting paths that later attempt to release associated buffers, especially when errors cause early returns.
Because Chi encourages composable middleware, the framework does not inherently manage ownership of context values for you. This places responsibility on the developer to ensure JWT-related structures are freed exactly once, regardless of how many handlers or error paths the request traverses.
Jwt Tokens-Specific Remediation in Chi — concrete code fixes
To prevent Double Free when working with JWT tokens in Chi, ensure that parsed token data is owned by a single responsible component and that cleanup logic is invoked in exactly one place. Prefer value semantics where possible, and avoid storing raw pointers or references to transient token objects in long-lived context values.
Example 1: Safe parsing and single cleanup
Parse the token once, store only necessary immutable data in the context, and avoid explicit freeing by relying on Go’s garbage collector. Do not attempt to manually free context values.
import (
"github.com/go-chi/chi/v5"
"github.com/golang-jwt/jwt/v5"
)
type ClaimsPayload struct {
UserID string `json:"user_id"`
jwt.RegisteredClaims
}
func JWTMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tokenString := extractToken(r)
token, err := jwt.ParseWithClaims(tokenString, &ClaimsPayload{}, func(token *jwt.Token) (interface{}, error) {
// return your key func
return []byte("secret"), nil
})
if err != nil || !token.Valid {
http.Error(w, "invalid token", http.StatusUnauthorized)
return
}
claims, ok := token.Claims.(*ClaimsPayload)
if !ok {
http.Error(w, "invalid claims", http.StatusUnauthorized)
return
}
// Attach only immutable or safe copies to context
ctx := context.WithValue(r.Context(), "claims", claims.UserID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func handler(w http.ResponseWriter, r *http.Request) {
userID := r.Context().Value("claims")
// Use userID, no manual cleanup needed
w.Write([]byte("OK"))
}
Example 2: Avoiding double cleanup in error paths
Ensure that any deferred cleanup functions do not operate on objects that may have already been released. If you store token metadata in a struct that requires cleanup, centralize that logic.
type TokenContext struct {
claims *ClaimsPayload
}
func (tc *TokenContext) Cleanup() {
// Centralized, idempotent cleanup
if tc.claims != nil {
tc.claims = nil // allow GC to reclaim
}
}
func main() {
r := chi.NewRouter()
r.Use(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tc := &TokenContext{}
defer tc.Cleanup() // safe: Cleanup is idempotent
token, err := jwt.ParseWithClaims(extractToken(r), &ClaimsPayload{}, keyFunc)
if err != nil {
http.Error(w, "bad token", http.StatusBadRequest)
return // Cleanup runs via defer, token not assigned
}
tc.claims = token.Claims.(*ClaimsPayload)
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), "tokenCtx", tc)))
})
})
r.Get("/secure", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("secure data"))
})
}
General guidelines
- Do not parse the same raw token string multiple times within a request; parse once and reuse the result.
- Avoid storing pointers to token objects in global or request-scoped caches that may be freed independently.
- Use Chi’s built-in context mechanisms to pass only lightweight, immutable data (e.g., user IDs) rather than full token objects.
- Structure middleware so that ownership of any allocated token-related structures is clearly vested in a single component with a single cleanup point.
By centralizing JWT parsing and avoiding multiple free paths, you eliminate the conditions that lead to Double Free while preserving the flexibility of Chi’s middleware composition.