Race Condition in Chi
How Race Condition Manifests in Chi
Race conditions in Chi occur when concurrent requests manipulate shared state without proper synchronization, leading to inconsistent or exploitable outcomes. In Chi-based applications, this typically manifests around user authentication flows, inventory management, and financial transactions.
A common pattern involves Chi's middleware chain where multiple requests attempt to modify the same session or database record simultaneously. Consider a banking application where two concurrent requests try to withdraw from the same account:
func withdrawHandler(w http.ResponseWriter, r *http.Request) {
var req WithdrawalRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
accountID := chi.URLParam(r, "account_id")
account, err := getAccountFromDB(accountID)
if err != nil {
http.Error(w, "Account not found", http.StatusNotFound)
return
}
// Race condition here - no locking
if account.Balance < req.Amount {
http.Error(w, "Insufficient funds", http.StatusForbidden)
return
}
account.Balance -= req.Amount
updateAccountInDB(account)
w.WriteHeader(http.StatusOK)
}Two concurrent requests could both pass the balance check before either completes the update, resulting in an overdraft. This becomes particularly dangerous in Chi applications handling inventory systems where stock levels are checked and updated without atomic operations.
Another Chi-specific manifestation involves route parameter manipulation combined with race conditions. When using chi.Route with middleware that sets context values, concurrent requests might interfere with each other's context if not properly isolated:
chi.Route("/api/v1/accounts/{account_id}", func(r chi.Router) {
r.Use(setAccountContext)
r.Put("/transfer", transferHandler)
})
func setAccountContext(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
accountID := chi.URLParam(r, "account_id")
account, _ := getAccountFromDB(accountID)
ctx := context.WithValue(r.Context(), accountContextKey, account)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func transferHandler(w http.ResponseWriter, r *http.Request) {
account := r.Context().Value(accountContextKey).(*Account)
// Race condition if multiple transfers happen simultaneously
account.Balance += r.Context().Value("transfer_amount")
updateAccountInDB(account)
}The context value is set once per request, but if the account object is shared across requests (e.g., cached in memory), concurrent modifications create race conditions that Chi's routing alone cannot prevent.
Chi-Specific Detection
Detecting race conditions in Chi applications requires both static analysis of the routing structure and dynamic testing of concurrent request handling. middleBrick's scanning engine specifically identifies race condition vulnerabilities in Chi applications through several techniques.
The scanner examines Chi's routing patterns to identify endpoints that handle mutable state without proper synchronization. It looks for patterns like:
chi.Route("/inventory/{item_id}", func(r chi.Router) {
r.Use(checkStockMiddleware)
r.Post("/purchase", purchaseHandler)
})
func checkStockMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
itemID := chi.URLParam(r, "item_id")
stock, _ := getStockFromDB(itemID)
if stock <= 0 {
http.Error(w, "Out of stock", http.StatusForbidden)
return
}
// Race condition: stock checked but not locked
r = r.WithContext(context.WithValue(r.Context(), stockKey, stock))
next.ServeHTTP(w, r)
})
}middleBrick flags this pattern because the stock check and purchase operation are not atomic. The scanner tests this by sending concurrent requests to the same endpoint with identical parameters, monitoring for inconsistent responses or state changes.
For Chi applications using middleware chains, middleBrick analyzes the middleware order and context propagation. It identifies dangerous patterns where context values are set in middleware but not properly isolated per request:
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
user, err := verifyToken(token)
if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Race condition if user object is shared
ctx := context.WithValue(r.Context(), userKey, user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func rateLimitMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user := r.Context().Value(userKey).(*User)
// Concurrent requests might all pass rate limit
if !checkRateLimit(user.ID) {
http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}The scanner tests these middleware chains by sending burst traffic and checking whether rate limits are properly enforced across concurrent requests. It also examines Chi's parameter binding and validation to ensure they're thread-safe when handling concurrent requests.
Chi-Specific Remediation
Remediating race conditions in Chi applications requires leveraging Go's concurrency primitives and Chi's middleware architecture properly. The most effective approach combines database-level locking with careful middleware design.
For database operations, use transactions with appropriate isolation levels. Chi's middleware can establish database transactions that span the entire request:
func transactionMiddleware(db *sql.DB) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tx, err := db.Begin()
if err != nil {
http.Error(w, "Transaction error", http.StatusInternalServerError)
return
}
ctx := context.WithValue(r.Context(), txKey, tx)
next.ServeHTTP(w, r.WithContext(ctx))
if w.Header().Get("X-Error") != "" {
tx.Rollback()
} else {
tx.Commit()
}
})
}
}
func purchaseHandler(w http.ResponseWriter, r *http.Request) {
tx := r.Context().Value(txKey).(*sql.Tx)
var req PurchaseRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Use SELECT FOR UPDATE to lock the row
var stock int
err := tx.QueryRow("SELECT stock FROM inventory WHERE item_id = $1 FOR UPDATE",
req.ItemID).Scan(&stock)
if err != nil || stock < req.Quantity {
http.Error(w, "Insufficient stock", http.StatusForbidden)
return
}
_, err = tx.Exec("UPDATE inventory SET stock = stock - $1 WHERE item_id = $2",
req.Quantity, req.ItemID)
if err != nil {
http.Error(w, "Update failed", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}This pattern ensures that concurrent purchase requests for the same item are serialized at the database level, preventing overselling.
For in-memory state management, use mutexes or channels to coordinate access. Chi's middleware can provide thread-safe access to shared resources:
type inventoryStore struct {
mu sync.RWMutex
items map[string]int
}
func (s *inventoryStore) getStock(itemID string) int {
s.mu.RLock()
defer s.mu.RUnlock()
return s.items[itemID]
}
func (s *inventoryStore) updateStock(itemID string, delta int) bool {
s.mu.Lock()
defer s.mu.Unlock()
current := s.items[itemID]
if current + delta < 0 {
return false
}
s.items[itemID] = current + delta
return true
}
func inventoryMiddleware(store *inventoryStore) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), storeKey, store)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
func purchaseHandler(w http.ResponseWriter, r *http.Request) {
store := r.Context().Value(storeKey).(*inventoryStore)
var req PurchaseRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if !store.updateStock(req.ItemID, -req.Quantity) {
http.Error(w, "Insufficient stock", http.StatusForbidden)
return
}
w.WriteHeader(http.StatusOK)
}This approach ensures that stock updates are atomic and thread-safe, even when multiple requests modify the same inventory item concurrently.