Race Condition in Echo Go
How Race Condition Manifests in Echo Go
Race conditions in Echo Go applications typically occur when multiple concurrent requests manipulate shared state without proper synchronization. In Echo's context, this often appears in account operations, inventory management, and financial transactions.
The most common race condition pattern in Echo Go involves account balance updates. Consider this vulnerable code:
func (s *Service) TransferFunds(c echo.Context) error {
var req TransferRequest
if err := c.Bind(&req); err != nil {
return err
}
srcUser, err := s.userRepo.GetByID(req.SourceID)
if err != nil {
return c.JSON(http.StatusNotFound, err)
}
// Race condition here - no locking
if srcUser.Balance < req.Amount {
return c.JSON(http.StatusForbidden, "insufficient funds")
}
srcUser.Balance -= req.Amount
destUser, _ := s.userRepo.GetByID(req.DestinationID)
destUser.Balance += req.Amount
s.userRepo.Update(srcUser)
s.userRepo.Update(destUser)
return c.JSON(http.StatusOK, "transfer successful")
}Two concurrent requests can both pass the balance check before either update completes, allowing the source account to go negative. This is particularly dangerous in financial applications built with Echo.
Another Echo-specific race condition occurs in inventory management endpoints:
func (h *ProductHandler) Purchase(c echo.Context) error {
id := c.Param("id")
qty := c.QueryParam("quantity")
product, err := h.repo.GetProduct(id)
if err != nil {
return c.JSON(http.StatusNotFound, err)
}
// Race condition - no atomic check-and-update
if product.Stock < qty {
return c.JSON(http.StatusForbidden, "insufficient stock")
}
product.Stock -= qty
h.repo.UpdateProduct(product)
return c.JSON(http.StatusOK, "purchase successful")
}During flash sales or high-traffic events, multiple Echo handlers can deplete stock below zero. The Echo framework's default behavior of handling requests concurrently amplifies this vulnerability.
Echo's middleware chain can also introduce race conditions. Consider a rate limiter that uses in-memory storage:
var requestCount = make(map[string]int)
func RateLimitMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
ip := c.RealIP()
count := requestCount[ip]
if count >= 100 {
return c.JSON(http.StatusTooManyRequests, "rate limit exceeded")
}
requestCount[ip] = count + 1
defer func() { requestCount[ip] = count + 1 }()
return next(c)
}
}This naive implementation allows multiple requests from the same IP to bypass the limit entirely due to unsynchronized map access. Echo's default goroutine-per-request model means this race condition affects all concurrent requests.
Echo Go-Specific Detection
Detecting race conditions in Echo applications requires both static analysis and runtime monitoring. For static detection, look for these Echo-specific patterns:
Shared State Without Synchronization: Search for maps, slices, or struct fields accessed across goroutines without mutexes. In Echo applications, this often appears in:
- Middleware that maintains state (rate limiters, session stores)
- Global variables used across handlers
- Caches implemented with in-memory storage
Database Transaction Patterns: Echo applications frequently use GORM or similar ORMs. Race conditions often occur when:
- Multiple SELECT queries precede an UPDATE without transaction isolation
- Application-level checks (balance verification) aren't atomic with state modification
- Optimistic locking isn't implemented for concurrent updates
API Endpoint Analysis: middleBrick's scanner can detect Echo-specific race condition indicators by analyzing your running API:
middlebrick scan https://api.example.com --output json
The scanner tests for BOLA (Broken Object Level Authorization) and BFLA (Broken Function Level Authorization) vulnerabilities that often accompany race conditions. For Echo applications specifically, it checks:
- Concurrent access to user-specific resources
- Inventory endpoints vulnerable to stock depletion
- Financial operations without proper locking
Runtime Monitoring: Implement logging to detect race conditions in production Echo applications:
func (s *Service) TransferFunds(c echo.Context) error {
var req TransferRequest
if err := c.Bind(&req); err != nil {
return err
}
logCtx := log.WithFields(log.Fields{
"source_id": req.SourceID,
"dest_id": req.DestinationID,
"amount": req.Amount,
"request_id": c.Response().Header().Get("X-Request-ID"),
})
srcUser, err := s.userRepo.GetByID(req.SourceID)
if err != nil {
logCtx.Warn("source user not found")
return c.JSON(http.StatusNotFound, err)
}
// Log pre-check state
logCtx.Debugf("pre-check balance: %d", srcUser.Balance)
if srcUser.Balance < req.Amount {
logCtx.Warn("insufficient funds")
return c.JSON(http.StatusForbidden, "insufficient funds")
}
// Use database transaction with row locking
tx := s.db.Begin()
defer tx.Rollback()
var srcForUpdate User
tx.Where("id = ?", req.SourceID).Set("gorm:query_option", "FOR UPDATE").First(&srcForUpdate)
if srcForUpdate.Balance < req.Amount {
logCtx.Warn("race condition prevented - balance changed")
return c.JSON(http.StatusForbidden, "insufficient funds")
}
srcForUpdate.Balance -= req.Amount
tx.Save(&srcForUpdate)
destUser, _ := s.userRepo.GetByID(req.DestinationID)
destUser.Balance += req.Amount
tx.Save(&destUser)
if err := tx.Commit().Error; err != nil {
logCtx.Error("transaction failed", err)
return c.JSON(http.StatusInternalServerError, "transfer failed")
}
logCtx.Info("transfer successful")
return c.JSON(http.StatusOK, "transfer successful")
}This pattern logs enough context to identify race conditions in production while using database-level locking to prevent them.
Echo Go-Specific Remediation
Remediating race conditions in Echo applications requires understanding Go's concurrency model and Echo's request handling patterns. Here are Echo-specific solutions:
Database-Level Locking: The most reliable approach for Echo applications is using database transactions with row-level locking:
func (s *Service) TransferFundsAtomic(c echo.Context) error {
var req TransferRequest
if err := c.Bind(&req); err != nil {
return err
}
tx := s.db.Begin()
defer tx.Rollback()
// Lock both rows for update
var src, dest User
tx.Where("id = ?", req.SourceID).Set("gorm:query_option", "FOR UPDATE").First(&src)
tx.Where("id = ?", req.DestinationID).Set("gorm:query_option", "FOR UPDATE").First(&dest)
if src.ID == 0 || dest.ID == 0 {
return c.JSON(http.StatusNotFound, "user not found")
}
if src.Balance < req.Amount {
return c.JSON(http.StatusForbidden, "insufficient funds")
}
src.Balance -= req.Amount
dest.Balance += req.Amount
tx.Save(&src)
tx.Save(&dest)
if err := tx.Commit().Error; err != nil {
return c.JSON(http.StatusInternalServerError, "transfer failed")
}
return c.JSON(http.StatusOK, "transfer successful")
}This pattern ensures that concurrent requests block until the transaction completes, eliminating the race condition entirely.
Echo Middleware for Synchronization: For application-level state that must be shared across requests, use Echo's middleware chain with proper synchronization:
type rateLimiter struct {
mu sync.RWMutex
counts map[string]int
limit int
}
func (r *rateLimiter) Allow(ip string) bool {
r.mu.Lock()
defer r.mu.Unlock()
count := r.counts[ip]
if count >= r.limit {
return false
}
r.counts[ip] = count + 1
return true
}
func RateLimitMiddleware(limit int) echo.MiddlewareFunc {
limiter := &rateLimiter{
counts: make(map[string]int),
limit: limit,
}
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
ip := c.RealIP()
if !limiter.Allow(ip) {
return c.JSON(http.StatusTooManyRequests, "rate limit exceeded")
}
return next(c)
}
}
}
// Use in Echo setup
e := echo.New()
e.Use(RateLimitMiddleware(100))Optimistic Locking with Versioning: For Echo applications using GORM, implement optimistic locking to detect and handle race conditions:
type User struct {
ID uint `gorm:"primarykey"`
Balance int64
Version int `gorm:"default:0"` // Optimistic locking field
UpdatedAt time.Time
}
func (s *Service) TransferWithOptimisticLock(c echo.Context) error {
var req TransferRequest
if err := c.Bind(&req); err != nil {
return err
}
for attempt := 0; attempt < 3; attempt++ {
srcUser, err := s.userRepo.GetByID(req.SourceID)
if err != nil {
return c.JSON(http.StatusNotFound, err)
}
if srcUser.Balance < req.Amount {
return c.JSON(http.StatusForbidden, "insufficient funds")
}
srcUser.Balance -= req.Amount
srcUser.Version++ // Increment version for optimistic locking
if err := s.userRepo.UpdateWithVersion(srcUser); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// Retry on version conflict
continue
}
return c.JSON(http.StatusInternalServerError, "transfer failed")
}
// Update destination separately or in a separate transaction
break
}
return c.JSON(http.StatusOK, "transfer successful")
}Echo-Specific Testing: Test your Echo application for race conditions using Go's race detector:
go test -race ./...
# Or run your Echo application with race detection
GORACE="history=4" go run -race main.go
Additionally, use middleBrick's CLI to scan your Echo API endpoints:
middlebrick scan http://localhost:8080 --output json --verbose
This will identify BOLA and BFLA vulnerabilities that often indicate underlying race condition issues in your Echo application.