Race Condition in Chi with Cockroachdb
Race Condition in Chi with Cockroachdb
A race condition in a Chi application using CockroachDB typically arises when multiple concurrent requests read and write the same row without appropriate serialization, allowing interleaved operations to produce inconsistent state. Chi is a lightweight router for Go that makes it easy to compose handlers; when handlers perform database logic without explicit locking or isolation, they expose timing-dependent behaviors.
Consider a balance transfer endpoint implemented in Chi. Two requests may concurrently read the same account row, compute a new balance based on the stale read, and write back their results. Because CockroachDB provides serializable isolation by default, a write-write conflict will be detected only at commit time. However, if the application does not retry the transaction, the conflict may surface as a lost update or a non-repeatable read. An attacker can orchestrate rapid concurrent calls to amplify the window where inconsistent reads occur, potentially bypassing application-level checks that assume sequential execution.
Insecure patterns include performing read–modify–write logic outside a transaction or using a non-unique or missing WHERE clause that broadens the affected rows. Without explicit transaction boundaries and proper error handling for CockroachDB’s retryable serializable errors, the race condition remains latent. middleBrick scans for such patterns under its BOLA/IDOR and Property Authorization checks, flagging endpoints where authorization or data integrity depends on timing.
Real-world examples involve endpoints that update financial ledgers, session tokens, or inventory quantities. For instance, if a handler reads a row, validates a business rule, and issues an UPDATE without holding a lock or using a conditional UPDATE, two requests may both pass validation and overwrite each other. This maps to OWASP API Top 10 A01:2023 Broken Object Level Authorization when the race condition permits privilege escalation or data leakage across users.
Cockroachdb-Specific Remediation in Chi
Remediation centers on ensuring that read–modify–write sequences are executed atomically within a CockroachDB transaction, with explicit error handling for serializable conflicts. Use the BEGIN, SELECT … FOR UPDATE, and COMMIT pattern to lock rows on read, or rely on conditional writes that encode the expected state.
Example 1: Atomic balance transfer with SELECT FOR UPDATE and retry logic.
import ( "context" "database/sql" "fmt" "net/http" "time" "github.com/go-chi/chi/v5" _ "github.com/cockroachdb/cockroach-go/v2/crdb" ) func transferBalance(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { fromID := chi.URLParam(r, "from") toID := chi.URLParam(r, "to") var amount float64 // decode JSON body into amount... err := crdb.ExecuteTx(r.Context(), db, &sql.TxOptions{ Isolation: sql.LevelSerializable, }, func(tx *sql.Tx) error { var fromBalance float64 row := tx.QueryRowContext(r.Context(), "SELECT balance FROM accounts WHERE id = $1 FOR UPDATE", fromID) if err := row.Scan(&fromBalance); err != nil { return fmt.Errorf("failed to read balance: %w", err) } if fromBalance < amount { return sql.ErrTxDone // will trigger a rollback } if _, err := tx.ExecContext(r.Context(), "UPDATE accounts SET balance = balance - $1 WHERE id = $2", amount, fromID); err != nil { return fmt.Errorf("debit failed: %w", err) } if _, err := tx.ExecContext(r.Context(), "UPDATE accounts SET balance = balance + $1 WHERE id = $2", amount, toID); err != nil { return fmt.Errorf("credit failed: %w", err) } return nil }) if err != nil { http.Error(w, "operation failed, retry if needed", http.StatusConflict) return } w.WriteHeader(http.StatusOK) } }Example 2: Conditional UPDATE without a separate read, using the expected current balance as a guard.
func updateBalanceConditional(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { accountID := chi.URLParam(r, "id") var expected float64 var newVal float64 // decode expected and newVal from request... ctx := r.Context() res, err := db.ExecContext(ctx, "UPDATE accounts SET balance = $1 WHERE id = $2 AND balance = $3", newVal, accountID, expected) if err != nil { http.Error(w, "database error", http.StatusInternalServerError) return } n, _ := res.RowsAffected() if n == 0 { http.Error(w, "balance mismatch, concurrent modification detected", http.StatusConflict) return } w.WriteHeader(http.StatusOK) } }In both examples, the Chi router passes the request context to CockroachDB calls, ensuring cancellation propagates correctly. The first example uses SELECT FOR UPDATE to lock rows explicitly; the second uses a conditional WHERE clause to make the update atomic. Both approaches eliminate the window for interleaved reads and writes. middleBrick’s checks for unsafe consumption and property authorization can highlight endpoints that lack these patterns.