Timing Attack in Gorilla Mux with Cockroachdb
Timing Attack in Gorilla Mux with Cockroachdb — how this specific combination creates or exposes the vulnerability
A timing attack in the combination of Gorilla Mux and Cockroachdb arises when the application performs user-authentication or record-retrieval flows where the server’s response time differs measurably based on whether a supplied identifier (e.g., user ID, API key, or tenant ID) exists in Cockroachdb. Gorilla Mux is a strict, pattern-based router; if route selection or subsequent database logic is not constant-time, an attacker can infer valid identifiers by measuring latency differences.
Consider a login or lookup endpoint that first queries Cockroachdb for a row by username or API key. If the query uses a simple WHERE clause and returns early when no row is found, the absence path is faster than the presence path (which may then proceed to password verification or additional joins). An attacker can send many candidate identifiers and observe response times to statistically infer valid usernames or keys. This is a classic information-leakage via side channels, and it is not a Cockroachdb weakness — it is how the application uses the database within Gorilla Mux handlers.
Specific contributing patterns include: branching on sql.ErrNoRows versus returning a generic delay, using SELECT * without limiting columns when only a boolean existence check is needed, and failing to ensure that any cryptographic or business logic step always executes for a comparable duration. Cockroachdb’s distributed nature does not inherently cause timing differences, but network latency and query planning variations can amplify subtle timing differences if the application does not normalize execution time.
Insecure code example that is vulnerable:
func getUserHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) username := vars["username"] var exists bool = false err := db.QueryRowContext(r.Context(), "SELECT EXISTS(SELECT 1 FROM users WHERE username = $1)", username).Scan(&exists) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if !exists { http.Error(w, "not found", http.StatusNotFound) return } // Only reached when user exists — this path is slower due to extra work // (e.g., password hash verification), leaking existence via timing. verifyPasswordAndRespond(w, r, username) }In the above, an attacker can reliably detect valid usernames because the “not found” path returns earlier and with less CPU work. Even with Cockroachdb, the difference in network round-trip plus query execution can be measurable, especially under controlled network conditions from the attacker.
Cockroachdb-Specific Remediation in Gorilla Mux — concrete code fixes
Remediation focuses on making the presence and absence paths take comparable time, regardless of whether the row exists. This includes constant-time existence checks, uniform work for both paths, and avoiding early returns that create timing divergence. Below are concrete, safe patterns for Gorilla Mux handlers using Cockroachdb.
1) Always perform a constant-time check and delay:
func getUserHandlerSecure(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) username := vars["username"] var dummy bool = false // Always perform the same query; do not branch on existence err := db.QueryRowContext(r.Context(), "SELECT EXISTS(SELECT 1 FROM users WHERE username = $1)", username).Scan(&dummy) // Constant small work regardless of result // Introduce a constant-time delay to mask timing differences if needed, e.g.: time.Sleep(50 * time.Millisecond) // Continue with uniform logic — do not early-return on existence // Fetch full record only after constant-time gate var full User err = db.QueryRowContext(r.Context(), "SELECT id, email, role FROM users WHERE username = $1", username).Scan(&full.ID, &full.Email, &full.Role) if err != nil { // Treat missing and internal errors uniformly to avoid leaking more info http.Error(w, "not found", http.StatusNotFound) return } // Safe to proceed with password verification or other work verifyPasswordAndRespond(w, r, &full) }2) Use parameterized queries and avoid SELECT * when only metadata is needed; keep the result shape identical across paths:
const existenceQuery = "SELECT CASE WHEN EXISTS (SELECT 1 FROM tenants WHERE tenant_id = $1) THEN '1'::text ELSE '0'::text END" func checkTenant(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) tid := vars["tenant_id"] var result string err := db.QueryRowContext(r.Context(), existenceQuery, tid).Scan(&result) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // Always take the same code path here if result == "1" { // proceed with authenticated action } else { http.Error(w, "access denied", http.StatusForbidden) } }3) If using prepared statements, ensure they are reused and that execution paths do not diverge based on row existence:
// Prepare once, reuse across requests. var userExistsStmt *sql.Stmt func init() { var err error userExistsStmt, err = db.PrepareContext(context.Background(), "SELECT 1 FROM users WHERE username = $1") if err != nil { log.Fatalf("prepare failed: %v", err) } } func handlerPrepared(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) username := vars["username"] var found int64 err := userExistsStmt.QueryRowContext(r.Context(), username).Scan(&found) // Always consume the result and apply constant follow-up logic if err != nil { // Use a generic error response to avoid leaking timing or existence details http.Error(w,