Timing Attack in Adonisjs with Cockroachdb
Timing Attack in Adonisjs with Cockroachdb — how this specific combination creates or exposes the vulnerability
A timing attack in the combination of AdonisJS and CockroachDB arises from observable differences in response time when processing valid versus invalid inputs, often due to branching logic that depends on database lookups. In AdonisJS, authentication and authorization flows frequently query CockroachDB to find a user by email or username. If the code uses a conditional such that a missing user results in an early return or a different code path than a found user, an attacker can measure precise response delays and infer whether a given email exists in the database.
With CockroachDB, which is PostgreSQL-wire compatible, the behavior of prepared statements and query planning can amplify subtle timing differences. For example, an AdonisJS controller that calls User.findBy('email', email) and then branches based on whether the result is null can introduce a measurable difference between a user that exists and one that does not, because the query execution plan may differ when a row is found versus when a sequential scan completes with zero rows. If the application performs additional synchronous work (such as password hashing with an intentionally slow algorithm) only in the found-user path, the timing gap becomes more pronounced across the network round trip and application processing.
Even when AdonisJS uses parameterized queries to CockroachDB, which protect against SQL injection, the application-layer branching remains a risk. Attackers can perform statistical analysis across many requests to reduce noise and distinguish valid accounts. This is particularly relevant for endpoints that return user-specific data or tokens after successful authentication, where the presence or absence of a row affects CPU usage, connection handling, or serialization steps in the AdonisJS request lifecycle.
Consider an endpoint that retrieves a user profile by public ID. If the route handler first fetches the record and then conditionally enriches it based on relationships, the time taken to join or skip related rows in CockroachDB can vary with data distribution and index presence. An attacker who can trigger these endpoints without authentication can probe timing differences to enumerate user IDs or infer relationships, especially if the IDs are sequential or otherwise predictable.
To mitigate timing-based information leakage in this stack, ensure that code paths for missing and found records take approximately the same amount of time. This includes avoiding early returns on authentication failures, standardizing response times, and reviewing query patterns for variability due to indexes or data presence. Constant-time comparison functions should be used for any sensitive tokens or hashes, and database interactions should avoid conditional logic that changes observable behavior based on row existence.
Cockroachdb-Specific Remediation in Adonisjs — concrete code fixes
Remediation focuses on making operations uniform regardless of whether a row exists. In AdonisJS, prefer methods that avoid branching on query results when handling authentication or sensitive lookups. Use parameterized queries to CockroachDB to prevent injection and ensure stable execution plans, and structure code so that the work done for valid and invalid inputs is consistent.
Example: secure user lookup and constant-time comparison for authentication tokens. Always perform a fixed-cost operation even when the user is not found.
import { DateTime } from 'luxon'
import User from '#models/user'
import { hash, compare } from '@adonisjs/core/services/hash'
export default class AuthController {
async login({ request, response }) {
const { email, password, token } = request.only(['email', 'password', 'token'])
// Fetch user with a parameterized query; CockroachDB driver uses prepared statements
const user = await User.query().where('email', email).limit(1).first()
// Compute a reference hash regardless of user existence to keep timing uniform
const referenceHash = await hash.make(password + '|fallback_salt')
const userHash = user ? await hash.make(user.passwordSalt + password) : referenceHash
// Constant-time comparison to avoid branching based on token validity
const tokenValid = user && (await compare(token, user.sessionToken))
const safeCompare = await compare(token, user ? user.sessionToken : referenceHash)
if (!safeCompare) {
return response.unauthorized({ message: 'Invalid credentials' })
}
// Standardized response shape and fixed-cost enrichment
const profile = {
id: user?.id ?? -1,
email: email,
role: user?.role ?? 'guest',
issuedAt: DateTime.now().toISO(),
}
return response.ok({ profile })
}
}
Example: avoid timing leaks when checking ownership or relationships. Use a single query that joins and validates in one round-trip to CockroachDB, rather than branching on whether a related row exists.
import Post from '#models/post'
export default class PostsController {
async show({ params, auth }) {
// Fetch post with an inner join to comments in one query to keep timing consistent
const post = await Post.query()
.select(['posts.id', 'posts.title', 'comments.id as comment_exists'])
.leftJoin('comments', 'comments.post_id', 'posts.id')
.where('posts.id', params.id)
.where('posts.user_id', auth.user?.id)
.limit(1)
.first()
// Uniform handling: post may be null, but the path is the same for all missing cases
if (!post) {
return response.notFound({ message: 'Post not found' })
}
return response.ok({ post })
}
}
Additional guidance: use environment-controlled query logging cautiously to avoid introducing timing variance via debug I/O, and prefer indexed columns on CockroachDB to keep execution times predictable. Regularly review controller actions that perform multiple conditional queries, and consolidate them into fewer, constant-cost operations where feasible.