Time Of Check Time Of Use in Chi with Api Keys
Time Of Check Time Of Use in Chi with Api Keys — how this specific combination creates or exposes the vulnerability
Time Of Check Time Of Use (TOCTOU) is a class of race condition that occurs when the outcome of a security decision depends on the timing between a check and the subsequent use of a resource. In Chi, this often intersects with API key validation patterns where an application first verifies the key’s validity or permissions and then performs an action based on that decision. If an attacker can mutate the underlying state between the check and the use, the security boundary enforced by the key can be bypassed.
Consider a Chi handler that validates an API key against a database or configuration before allowing a sensitive operation, such as modifying a user profile or initiating a financial transaction. A common pattern is to look up the key, confirm it is active, and then proceed with the request. However, if the lookup and the operation are not performed atomically, an attacker with the ability to modify the key’s associated state (for example, revocation or scope changes) between the check and the use can exploit the window to act under a now-invalid or downgraded privilege context.
Chi’s routing and middleware model encourages explicit handler composition, which can inadvertently create TOCTOU scenarios when key validation is split across multiple steps. For instance, an API key might be verified in an authentication middleware that sets a user context, and later handlers assume the context remains trustworthy. If the key is rotated or revoked after the middleware sets the context but before the handler processes the request, the handler may execute with stale authorization data. This is especially risky when the handler performs idempotent or high-impact operations without re-verifying the key immediately prior to the action.
Another scenario involves rate limiting and quota checks that rely on cached key metadata. If a key’s limits are checked and then the request proceeds to mutate state without reconfirming the key’s current status, an attacker might exploit timing to exceed quotas or bypass intended throttling. The vulnerability is not in Chi itself but in how developers structure key validation and request handling. Because Chi does not enforce a single execution model, it is up to the developer to ensure that checks and uses are tightly coupled, ideally within a single transactional boundary or with short-lived, cryptographically verifiable tokens that minimize the window of inconsistency.
Real-world attack patterns mirror classic TOCTOU exploits: an attacker monitors key rotation events or revocation lists and triggers malicious requests in the gap between check and use. In systems where API keys grant elevated permissions (e.g., admin scopes or write access to sensitive endpoints), this can lead to unauthorized data modification or exposure. Because Chi applications often integrate with databases, message queues, or external services, the impact of a successful TOCTOU can extend beyond the immediate handler to affect downstream systems that rely on the assumed integrity of the key.
To mitigate these risks, developers should design Chi handlers to perform authorization as close as possible to the point of use, avoiding long-lived contexts derived from initial key validation. When API keys are used, they should be treated as opaque tokens whose permissions are verified on each sensitive operation, or replaced with short-lived signed tokens that embed authorization claims and cannot be silently altered. Structural patterns such as wrapping sensitive logic in a single handler chain or using request-scoped contexts that cannot be mutated externally can reduce the window for race conditions. Understanding the interplay between Chi’s routing model and key management practices is essential to preventing TOCTOU vulnerabilities.
Api Keys-Specific Remediation in Chi — concrete code fixes
Remediation focuses on ensuring that authorization checks and the corresponding actions are performed atomically and that API key validation is as close as possible to the point of use. In Chi, this typically means avoiding global or request-scoped caches of key permissions that can become stale and instead validating the key within the handler or within a tightly coupled middleware chain that does not rely on mutable external state.
One concrete approach is to create a middleware that validates the API key and immediately attaches a verified authorization payload to the request context, ensuring that subsequent handlers rely only on this freshly verified context. The following example demonstrates a Chi routes setup where an API key is validated per request and a minimal, request-bound context is used:
// Chi middleware that validates an API key on each request
package main
import (
"context"
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
type authKey string
type claims struct {
Scope string
// other verified claims
}
func apiKeyValidator(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
provided := r.Header.Get("X-API-Key")
if provided == "" {
http.Error(w, `{"error":"missing api key"}`, http.StatusUnauthorized)
return
}
// Perform synchronous validation on each request.
// Replace with your key lookup logic (e.g., database or KMS).
valid, scope, err := validateKey(provided)
if err != nil || !valid {
http.Error(w, `{"error":"invalid api key"}`, http.StatusUnauthorized)
return
}
// Attach verified claims to a new context; do not reuse a mutable global cache.
ctx := context.WithValue(r.Context(), authKey("claims"), &claims{Scope: scope})
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func validateKey(key string) (bool, string, error) {
// Example stub: in production, check against a data source.
// Ensure this call is fast and does not introduce race conditions.
if key == "valid-key-123" {
return true, "write", nil
}
return false, "", nil
}
func sensitiveHandler(w http.ResponseWriter, r *http.Request) {
claims, ok := r.Context().Value(authKey("claims")).(*claims)
if !ok {
http.Error(w, `{"error":"unauthorized"}`, http.StatusForbidden)
return
}
if claims.Scope != "write" {
http.Error(w, `{"error":"insufficient scope"}`, http.StatusForbidden)
return
}
// Proceed with the operation, knowing the key was validated moments ago.
fmt.Fprintf(w, `{"status":"ok"}`)
}
func main() {
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(middleware.Logger)
r.Use(apiKeyValidator)
r.Post("/resource", sensitiveHandler)
http.ListenAndServe(":8080", r)
}
This pattern ensures that each request validates the API key independently and attaches a short-lived context that handlers trust. It avoids storing key state in global variables that could be altered concurrently and keeps the check and use within the same request lifecycle, reducing the TOCTOU window.
For higher assurance, consider embedding authorization in signed tokens issued after key validation. The token can carry scope and expiration, and handlers can verify signatures without repeated database lookups. However, the token must be issued within the same authenticated flow and must not be long-lived. The following illustrates issuing a token after key validation and using it in subsequent requests:
// Example of issuing a short-lived token after API key validation
package main
import (
"context"
"fmt"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/golang-jwt/jwt/v5"
)
var jwtSecret = []byte("super-secret-key")
type apiKeyService interface {
Lookup(key string) (bool, string, error)
}
type mockService struct{}
func (m mockService) Lookup(key string) (bool, string, error) {
if key == "valid-key-123" {
return true, "write", nil
}
return false, "", nil
}
func issueTokenMiddleware(svc apiKeyService) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
key := r.Header.Get("X-API-Key")
valid, scope, err := svc.Lookup(key)
if err != nil || !valid {
http.Error(w, `{"error":"invalid key"}`, http.StatusUnauthorized)
return
}
// Create a short-lived token with the verified scope.
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"scope": scope,
"exp": time.Now().Add(5 * time.Minute).Unix(),
})
tokenString, err := token.SignedString(jwtSecret)
if err != nil {
http.Error(w, `{"error":"token issue"}`, http.StatusInternalServerError)
return
}
// Return token to client; client must present it in subsequent requests.
w.Header().Set("Authorization", "Bearer "+tokenString)
next.ServeHTTP(w, r)
})
}
}
func main() {
svc := mockService{}
r := chi.NewRouter()
r.Use(issueTokenMiddleware(svc))
r.Get("/resource", func(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
if auth == "" {
http.Error(w, `{"error":"missing token"}`, http.StatusUnauthorized)
return
}
// Verify token signature and claims.
parsed, err := jwt.Parse(auth, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method")
}
return jwtSecret, nil
})
if err != nil || !parsed.Valid {
http.Error(w, `{"error":"invalid token"}`, http.StatusUnauthorized)
return
}
claims, ok := parsed.Claims.(jwt.MapClaims)
if !ok || claims["scope"] != "write" {
http.Error(w, `{"error":"forbidden"}`, http.StatusForbidden)
return
}
fmt.Fprintf(w, `{"status":"ok"}`)
})
http.ListenAndServe(":8080", r)
}
These examples illustrate how to bind validation closely to usage, minimizing the TOCTOU window. They also highlight the importance of avoiding shared mutable state for key validity and preferring per-request verification or short-lived, verifiable assertions that cannot be altered after issuance.