Timing Attack in Echo Go with Cockroachdb
Timing Attack in Echo Go with Cockroachdb — how this specific combination creates or exposes the vulnerability
A timing attack in the combination of Echo Go and Cockroachdb can occur when authentication or lookup logic in Go handlers exhibits data-dependent branching or memory access patterns that vary in execution time. Cockroachdb, like many distributed SQL databases, may return slightly different latencies based on query execution paths, index presence, or contention. If an Echo Go handler performs user lookup and then runs a conditional branch based on whether a row exists or matches, an attacker can infer information by measuring response times.
For example, consider a login handler that queries Cockroachdb for a username and then compares the provided password with the stored hash using a naive string comparison. In Go, == on byte slices or strings does not run in constant time; it short-circuits on the first differing byte. If the handler only queries Cockroachdb after a missing user is detected, the timing difference between a valid user (extra hash computation) and an invalid user (no hash computation) becomes measurable over the network. Cockroachdb query latencies may further amplify this when network conditions or node lease states introduce small variations that align with the attacker’s measurements.
An attacker can send many crafted requests while observing round-trip times. Queries that involve index seeks versus full scans in Cockroachdb may exhibit different latencies, and if the Go handler’s control flow depends on result presence or row values, these differences leak information about valid usernames or the structure of stored data. In a distributed setup, secondary indexes and range splits can also cause subtle timing variations that correlate with the presence or absence of data.
To illustrate, here is an insecure Echo Go login snippet that is vulnerable because it branches on user existence and performs a password comparison only when the user is found:
package main
import (
"context"
"crypto/sha256"
"fmt"
"net/http"
"time"
"github.com/labstack/echo/v4"
"github.com/lib/pq"
)
type User struct {
Username string
Password []byte // stored hash
}
func insecureLogin(c echo.Context) error {
username := c.FormValue("username")
password := c.FormValue("password")
ctx := context.Background()
connStr := "postgresql://user:pass@localhost:26257/db?sslmode=disable"
db, err := pq.OpenConnector(connStr)
if err != nil {
return c.String(http.StatusInternalServerError, "db error")
}
defer db.Close()
var storedHash []byte
row := db.QueryRowContext(ctx, "SELECT password FROM users WHERE username = $1", username)
err = row.Scan(&storedHash)
if err != nil {
// Simulate work to hide timing differences (still not safe)
time.Sleep(5 * time.Millisecond)
return c.String(http.StatusUnauthorized, "invalid credentials")
}
// Vulnerable comparison: not constant-time
if len(storedHash) == len([]byte(password)) && storedHash == []byte(password) {
return c.String(http.StatusOK, "welcome")
}
return c.String(http.StatusUnauthorized, "invalid credentials")
}
In this example, the query to Cockroachdb returns at different speeds depending on whether the username exists. The subsequent password comparison also branches early on length mismatch. An attacker can combine timing measurements with error behavior to infer valid usernames. Even added sleeps are insufficient if the comparison itself is data-dependent.
Cockroachdb-Specific Remediation in Echo Go — concrete code fixes
Remediation focuses on ensuring that all code paths with database interactions execute in constant time relative to attacker-controlled inputs, and that error handling does not introduce timing or behavioral distinctions. In Echo Go, always perform a constant-time comparison after retrieving a record, and ensure queries that fetch placeholder data when a user is not found do not introduce new timing leaks.
Use a constant-time comparison function for secrets. In Go, you can use crypto/subtle’s ConstantTimeCompare. Also, make the database query path uniform: either always retrieve a row (with a hashed placeholder) or always perform a dummy hash computation when the user is not found, so that total execution time and observable behavior remain consistent.
Here is a hardened version of the login handler using Cockroachdb via database/sql with pq driver, constant-time comparison, and uniform execution:
package main
import (
"context"
"crypto/sha256"
"crypto/subtle"
"database/sql"
"fmt"
"net/http"
"time"
"github.com/labstack/echo/v4"
_ "github.com/lib/pq"
)
type User struct {
Username string
Password []byte
}
// constantTimeCompare performs a constant-time comparison of two byte slices.
func constantTimeCompare(a, b []byte) bool {
return subtle.ConstantTimeCompare(a, b) == 1
}
// dummyHash returns a fixed-length dummy hash to simulate work when user is not found.
func dummyHash() []byte {
// A fixed-length deterministic dummy value; avoid variable length branches.
h := sha256.Sum256([]byte("dummy-salt"))
return h[:]
}
func secureLogin(c echo.Context) error {
username := c.FormValue("username")
password := c.FormValue("password")
ctx := context.Background()
connStr := "postgresql://user:pass@localhost:26257/db?sslmode=disable"
db, err := sql.Open("postgres", connStr)
if err != nil {
return c.String(http.StatusInternalServerError, "db error")
}
defer db.Close()
var storedHash []byte
row := db.QueryRowContext(ctx, "SELECT password FROM users WHERE username = $1", username)
err = row.Scan(&storedHash)
if err != nil {
if err == sql.ErrNoRows {
// Always perform a dummy hash to keep timing uniform
storedHash = dummyHash()
} else {
// Log and return a generic error without leaking timing distinctions
return c.String(http.StatusInternalServerError, "server error")
}
}
// Derive hash of provided password with the same parameters as stored hash.
// For simplicity, assume storedHash includes salt and algorithm info.
// In practice, use a proper KDF like bcrypt, scrypt, or argon2.
h := sha256.Sum256([]byte(password))
inputHash := h[:]
if constantTimeCompare(storedHash, inputHash) {
return c.String(http.StatusOK, "welcome")
}
// Always return the same generic response and ensure constant-time execution
return c.String(http.StatusUnauthorized, "invalid credentials")
}
Key points:
- Use
sql.ErrNoRowsto detect missing users and replace the missing row with a deterministic dummy hash so that the total work and timing do not reveal user presence. - Compare hashes with
subtle.ConstantTimeCompareto prevent data-dependent branch timing leaks in Go. - Keep database query patterns consistent; avoid branching on query results in ways that change execution paths or latencies observable over the network.
- For production, prefer a dedicated password hashing function (e.g., bcrypt, argon2) rather than raw SHA256, and ensure salts and work factors are handled uniformly.
Additionally, apply framework-level protections in Echo Go: set timeouts, avoid verbose errors, and standardize response times for authentication endpoints. These measures reduce the signal available to an attacker attempting to correlate timing with user existence or password validity.