Race Condition in Fiber with Cockroachdb
Race Condition in Fiber with Cockroachdb — how this specific combination creates or exposes the vulnerability
A race condition in a Fiber service using CockroachDB typically arises when multiple concurrent requests read and write the same database rows without strict serialization or application-level locking. Because CockroachDB provides strong consistency and serializable isolation by default, the database itself prevents anomalies such as lost updates at the SQL level. However, application logic that performs a read, computes a new value, and then writes based on that read can still be vulnerable when executed in separate transactions across concurrent requests.
Consider a balance update flow in Fiber where a handler reads an account row, adds a transfer amount, and writes back the new balance. If two requests interleave as follows—Request A reads balance = 100, Request B reads balance = 100, Request A writes balance = 150, Request B writes balance = 120—the final balance is incorrect (120 instead of 150). This is a classic lost update, and it is exposed because the handler does not serialize the read–modify–write within a single transaction or use conditional writes.
In a Fiber app, this often maps to the following anti-pattern: a route handler opens a transaction, performs a SELECT, returns a response or does additional non-database work, and then performs an UPDATE. If the handler is invoked concurrently, the interleaved operations can violate invariants. CockroachDB’s serializable isolation will cause one of the concurrent serializable transactions to abort with a serialization error if they write the same keys, which can surface as a retryable failure to clients. While this prevents corruption, it does not guarantee correctness for business logic unless the application handles retries and uses write conditions or explicit locking to enforce intent.
Another scenario involves optimistic concurrency control where a version or timestamp column is read and then checked on update. If the client-side logic reuses a stale version because of caching or delayed requests, the update may succeed incorrectly, leading to overwrites. Additionally, unbounded or long-polling endpoints that trigger background jobs on request data can exacerbate the issue when those jobs run with a slightly delayed snapshot of the database state.
To detect such patterns, middleBrick’s 12 security checks run in parallel and can surface findings related to authentication, BOLA/IDOR, and unsafe consumption that may indicate missing controls around concurrent access. It also cross-references OpenAPI/Swagger specs (2.0, 3.0, 3.1) with runtime behavior, ensuring definitions align with how endpoints interact with CockroachDB under load.
Cockroachdb-Specific Remediation in Fiber — concrete code fixes
Remediation focuses on ensuring read–modify–write sequences are executed as a single serializable transaction with conditional writes or explicit row locking. Below are concrete Fiber handler examples using the pgx driver and database/sql in Go, demonstrating two safe patterns.
1. Serializable transaction with conditional write
This pattern performs the balance adjustment in one transaction and uses a WHERE clause to ensure the update only applies when the expected version or balance matches. If the condition fails, the transaction aborts and can be retried by the client or middleware.
// Using database/sql with CockroachDB
func transferBalanceFiber(c *fiber.Ctx) error {
db, _ := sql.Open("pgx", "postgresql://user:pass@localhost:26257/defaultdb?sslmode=disable")
type Req struct {
AccountID int64 `json:\"account_id\"`
Amount int64 `json:\"amount\"`
}
var req Req
if err := c.BodyParser(req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
}
tx, err := db.Begin()
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
defer tx.Rollback()
var currentBalance int64
// Read within the transaction
err = tx.QueryRowContext(c.Context(), "SELECT balance FROM accounts WHERE id = $1 FOR UPDATE", req.AccountID).Scan(¤tBalance)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "account not found"})
}
newBalance := currentBalance + req.Amount
// Conditional update to prevent lost updates
res, err := tx.ExecContext(c.Context(), "UPDATE accounts SET balance = $1 WHERE id = $2 AND balance = $3", newBalance, req.AccountID, currentBalance)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
rowsAffected, _ := res.RowsAffected()
if rowsAffected == 0 {
tx.Rollback()
return c.Status(fiber.StatusConflict).JSON(fiber.Map{"error": "concurrent modification detected, please retry"})
}
if err := tx.Commit(); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"balance": newBalance})
}
2. Explicit SELECT FOR SHARE or FOR UPDATE for critical sections
When you need to lock rows to enforce strict ordering or prevent reads during a pending write, use SELECT FOR SHARE (allows reads but blocks writes) or SELECT FOR UPDATE (blocks both reads and writes for modification). This ensures that concurrent transactions are serialized on the locked rows.
// Using pgx directly within a Fiber handler
func getAndLockAccount(c *fiber.Ctx) error {
conn, err := pgx.Connect(context.Background(), "postgres://user:pass@localhost:26257/defaultdb?sslmode=disable")
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
defer conn.Close(context.Background())
var acc Account
// Acquire a row lock to prevent concurrent updates
err = conn.QueryRow(context.Background(), "SELECT id, balance, version FROM accounts WHERE id = $1 FOR UPDATE", req.AccountID).Scan(&acc.ID, &acc.Balance, &acc.Version)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "account not found"})
}
// Perform business logic with the locked row
acc.Balance += req.Amount
_, err = conn.Exec(context.Background(), "UPDATE accounts SET balance = $1, version = version + 1 WHERE id = $2", acc.Balance, acc.ID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(acc)
}
Additional recommendations: implement idempotency keys for retries, use exponential backoff in clients when encountering serialization errors, and keep transactions short to reduce contention. middleBrick’s CLI (middlebrick scan