HIGH side channel attackfibercockroachdb

Side Channel Attack in Fiber with Cockroachdb

Side Channel Attack in Fiber with Cockroachdb — how this specific combination creates or exposes the vulnerability

A side channel attack in the context of a Fiber application using CockroachDB does not exploit a bug in CockroachDB itself, but rather observes indirect, timing-based signals emitted by the interaction between the web framework and the database. In a typical Fiber handler, an attacker can measure the time it takes for an HTTP response to be returned after sending crafted requests. Variations in latency reveal whether a requested record exists, whether a row-level security (RLS) policy evaluated to true or false, or whether an index lookup succeeded. Because CockroachDB is a distributed SQL system that may exhibit variable latency due to replication, leaseholder routing, and consensus rounds, these timing differences are often more pronounced than with single-node databases.

Consider a login endpoint implemented in Fiber that queries CockroachDB by username:

// Insecure example: timing leak via username enumeration
app.Post("/login", func(c *fiber.Ctx) error {
  var user User
  username := c.FormValue("username")
  password := c.FormValue("password")
  // Direct string comparison enables timing attacks
  err := db.Get(c.Context(), &user, "SELECT id, password_hash FROM users WHERE username = $1", username)
  if err != nil {
    return c.SendStatus(fiber.StatusUnauthorized)
  }
  if subtle.ConstantTimeCompare([]byte(user.PasswordHash), []byte(hashPassword(password))) != 1 {
    return c.SendStatus(fiber.StatusUnauthorized)
  }
  return c.SendStatus(fiber.StatusOK)
})

If CockroachDB takes longer to return no rows versus a matching row (e.g., due to index presence or absence), an attacker can infer valid usernames by measuring response times. Once a valid username is identified, the attacker can focus on password guessing. The same pattern applies to endpoints that fetch user-specific data such as /profile or /settings, where missing rows versus present rows create distinguishable timing profiles. Because the database is distributed, network path, leaseholder location, and contention can add noise, but statistical analysis over many requests can still expose these correlations.

Another vector involves IDOR-like checks implemented at the application layer. For example, a handler might first check ownership before returning a resource:

// Insecure existence check leaks via timing
app.Get("/document/:id", func(c *fiber.Ctx) error {
  docID := c.Params("id")
  userID := c.Locals("userID").(int64)
  var count int
  db.Get(c.Context(), &count, "SELECT COUNT(*) FROM documents WHERE id = $1 AND owner_id = $2", docID, userID)
  if count == 0 {
    return c.SendStatus(fiber.StatusNotFound)
  }
  var doc Document
  db.Get(c.Context(), &doc, "SELECT * FROM documents WHERE id = $1", docID)
  return c.JSON(fiber.Map{"document": doc})
})

An attacker can probe with numeric IDs and observe whether responses are slightly faster for IDs that do not exist versus those the user is not allowed to see, because the COUNT query returns zero quickly when an index lookup misses or when the RLS policy evaluates to false without fetching the row. Even with CockroachDB’s strong consistency, these micro-timing differences can be measurable across the network, enabling enumeration of document IDs and inferring access boundaries without explicit authorization failures.

LLM/AI security probes are not directly relevant to this specific vector, but output handling that inadvertently echoes database structure or timing-sensitive error messages can compound the risk. The core issue is that the application reveals information through observable latency rather than explicit errors, and CockroachDB’s behavior under load can amplify these signals.

Cockroachdb-Specific Remediation in Fiber — concrete code fixes

Remediation focuses on ensuring that every path that interacts with CockroachDB takes a constant amount of time, regardless of data existence or permissions. The primary technique is to replace conditional early exits with uniform execution flows, and to avoid branching on sensitive data in application code.

For the login example, do not compare passwords only when the row exists. Instead, always fetch a hash (using a placeholder hash when the user does not exist) and perform a constant-time comparison:

// Secure: constant-time login handling
app.Post("/login", func(c *fiber.Ctx) error {
  username := c.FormValue("username")
  password := c.FormValue("password")
  var user User
  // Always select, even if user does not exist; use a default hash
  rowErr := db.Get(c.Context(), &user, "SELECT id, password_hash FROM users WHERE username = $1", username)
  var hashToCheck string
  if rowErr != nil {
    // Use a known dummy hash to keep timing consistent
    hashToCheck = "$2a$10$dummyplaceholderhashdummyplaceholderhash"
  } else {
    hashToCheck = user.PasswordHash
  }
  if subtle.ConstantTimeCompare([]byte(hashToCheck), []byte(hashPassword(password))) != 1 {
    return c.SendStatus(fiber.StatusUnauthorized)
  }
  return c.SendStatus(fiber.StatusOK)
})

This ensures that the database query path and the password comparison path do not reveal whether the username exists. The dummy hash must be of similar computational cost to a real hash to prevent timing variations in the comparison step itself.

For the document access pattern, avoid branching on count. Always perform the second query, or use a single query that returns the document if and only if the user is allowed, while ensuring the query plan and execution time remain consistent:

// Secure: constant-time document fetch with ownership check in one query
app.Get("/document/:id", func(c *fiber.Ctx) error {
  docID := c.Params("id")
  userID := c.Locals("userID").(int64)
  var doc Document
  // One query that either returns the row or no row, with constant-time behavior
  err := db.Get(c.Context(), &doc, `
    SELECT d.* FROM documents d
    JOIN LATERAL (SELECT 1 WHERE d.id = $1 AND d.owner_id = $2) AS chk ON true
    WHERE chk IS NOT NULL
  `, docID, userID)
  if err != nil {
    // Return a generic document-shaped empty response with the same serialization cost
    return c.JSON(fiber.Map{"document": Document{}})
  }
  return c.JSON(fiber.Map{"document": doc})
})

Alternatively, if you must keep two queries, ensure the first query always consumes similar time by fetching a dummy row when access is denied:

// Secure: constant-time two-query approach
app.Get("/document/:id", func(c *fiber.Ctx) error {
  docID := c.Params("id")
  userID := c.Locals("userID").(int64)
  var count int
  db.Get(c.Context(), &count, "SELECT COUNT(*) FROM documents WHERE id = $1 AND owner_id = $2", docID, userID)
  // Always proceed to a similar-cost read path
  var doc Document
  if count == 0 {
    db.Get(c.Context(), &doc, "SELECT * FROM documents WHERE id = -1") // dummy id that returns empty
  } else {
    db.Get(c.Context(), &doc, "SELECT * FROM documents WHERE id = $1", docID)
  }
  if doc.ID == 0 {
    return c.JSON(fiber.Map{"document": Document{}})
  }
  return c.JSON(fiber.Map{"document": doc})
})

Additionally, consider using middleware in Fiber to set consistent response headers and to apply uniform serialization routines so that response sizes and timing do not vary with data content. When using the middleBrick CLI (middlebrick scan <url>) or the GitHub Action to integrate API security checks into your CI/CD pipeline, you can detect such timing-leak patterns in your OpenAPI specs and runtime tests, ensuring these issues are surfaced before deployment.

Frequently Asked Questions

Can a side channel attack infer valid usernames even if the endpoint returns the same HTTP status code?
Yes. Even with identical status codes, differences in response time can leak whether a username exists, because database index lookups and RLS policy evaluation have variable cost. Use constant-time comparison and avoid branching on sensitive data to mitigate.
Does using CockroachDB’s strong consistency prevent timing-based information leaks?
No. Strong consistency ensures correctness of data, but it does not eliminate timing variations caused by index presence, network latency, or query execution paths. Application-level constant-time patterns are still required.