Password Spraying in Adonisjs with Cockroachdb
Password Spraying in Adonisjs with Cockroachdb — how this specific combination creates or exposes the vulnerability
Password spraying is an authentication attack where an adversary uses a small list of common passwords against many accounts. When Adonisjs applications use Cockroachdb as the backing store without accounting for protocol-level behavior and framework-specific patterns, certain configurations can make spraying easier to execute or harder to detect.
Adonisjs relies on the underlying database driver to manage connections and query execution. With Cockroachdb, the Postgres wire protocol is used, and connection pooling characteristics can influence how quickly concurrent login attempts are issued. If rate limiting is enforced only at the application layer and not backed by coordinated controls at the database or API gateway, an attacker can open many authenticated sessions to the Cockroachdb cluster and have each attempt complete a round-trip query, making timing-based defenses less effective.
In Adonisjs, authentication typically involves the auth module and guards that query a model. A common pattern is a User model with an email and password column. If the login action does not enforce per-IP or per-account attempt throttling before invoking the model query, each request results in a distinct SQL statement against Cockroachdb. Because Cockroachdb distributes rows across nodes, even a single logical query may touch multiple nodes, but the application still incurs a measurable round-trip for each attempt. This measurable latency and the ability to open many sessions enable an attacker to iterate passwords across accounts without triggering lockouts quickly.
Another vector arises from how Adonisjs handles asynchronous operations and retries. If the application or surrounding infrastructure automatically retries failed requests (for example, due to transient network errors), a password spraying campaign can inadvertently be amplified. Cockroachdb’s strong consistency guarantees mean each query either commits or aborts; however, the application may not differentiate between a bad password and a transient error unless explicit handling is implemented. Without precise status code mapping and error suppression for authentication-specific failures, retries can increase the volume of attempts seen by Cockroachdb.
Additionally, if audit logging or monitoring relies on SQL-level observability (e.g., changefeeds or database-side logging), an attacker can infer timing patterns or account existence based on response metadata. Even when the application returns a generic message like “invalid credentials,” differences in query plan execution or index usage on the Cockroachdb side can introduce subtle timing variations. These variations, combined with a slow but steady spray across many accounts, can allow an attacker to triangulate valid accounts without triggering threshold-based alerts.
To summarize, the combination of Adonisjs authentication flows, Cockroachdb’s distributed query execution, and insufficient rate limiting or anomaly detection creates an environment where low-and-slow password spraying can be effective. The attack surface is not limited to the web layer; it spans the database driver, connection handling, and operational observability, making coordinated defenses across the stack necessary.
Cockroachdb-Specific Remediation in Adonisjs — concrete code fixes
Remediation focuses on reducing the effectiveness of spraying by enforcing strong rate controls, standardizing error responses, and ensuring that authentication logic does not leak timing or account information through database behavior.
First, apply request-level throttling before any database operation. Use a shared key that combines the target account identifier and the client IP to ensure per-account, per-IP limits. This prevents a single client from iterating through many accounts quickly.
import { middleware } from '@adonisjs/core/http'
import { RateLimiterRedis } from 'rate-limiter-flexible'
import { Redis } from '@socket.io/redis-client'
import { application } from '@adonisjs/core/app'
const redisClient = new Redis({
host: 'your-redis-host',
port: 6379,
})
const rateLimiter = new RateLimiterRedis({
storeClient: redisClient,
keyPrefix: 'auth_attempts',
points: 5, // 5 attempts
duration: 60, // per 60 seconds
blockDuration: 300, // block for 5 minutes if exceeded
})
export const authRateLimit = async (ctx, next) => {
const { email, ip } = ctx.request.body()
const safeEmail = email ? email.trim().toLowerCase() : 'unknown'
const identifier = `${safeEmail}:${ip}`
try {
await rateLimiter.consume(identifier)
} catch (rateError) {
// Always return the same generic response
return ctx.response.unauthorized({ message: 'Invalid credentials' })
}
await next()
}
Second, ensure that the login route uses a constant-time comparison for passwords and does not branch on account existence. In Adonisjs, this means using the same code path regardless of whether the email is found.
import Route from '@ioc:Adonis/Core/Route'
import { schema } from '@ioc:Adonis/Core/Validator'
import User from 'App/Models/User'
import { Hash } from '@ioc:Adonis/Core/Hash'
Route.post('/login', async ({ request, response }) => {
const payload = request.validate({
schema: schema.create({
email: schema.string({ trim: true, normalize: true }),
password: schema.string.optional(),
}),
})
// Always fetch a record; if missing, use a dummy instance to keep timing consistent
const user = await User.query()
.where('email', payload.email.toLowerCase())
.limit(1)
.preload('roles')
.first()
const dummyUser = new User()
const target = user || dummyUser
// Constant-time check
const passwordMatch = Hash.verify(payload.password, target.password || '')
if (!user || !passwordMatch) {
return response.unauthorized({ message: 'Invalid credentials' })
}
// Generate session/token only for valid account
const token = await user.related('tokens').create({ type: 'api' })
return response.ok({ token })
})
Third, coordinate with Cockroachdb-side settings to reduce noisy retries and ensure predictable behavior. Use application-level locks or conditional writes to avoid amplifying requests due to transient errors. For example, when updating failed attempts, use an upsert that avoids race conditions across the distributed nodes.
import { DateTime } from 'luxon'
import { AuditLog } from 'App/Models/AuditLog'
export const recordAuthAttempt = async (email: string, success: boolean, ip: string) => {
await AuditLog.updateOrCreate(
{ email, ip, date: DateTime.local().toISODate() },
{
email,
ip,
date: DateTime.local().toISODate(),
attempts: success ? 0 : AuditLog.raw('COALESCE(attempts, 0) + 1'),
lastAttemptAt: DateTime.local().toSQL()!,
}
)
}
Finally, integrate middleBrick to validate that your remediation does not introduce new risks. Use the CLI to scan endpoint definitions and confirm that authentication routes are not overly permissive. On the Pro plan, enable continuous monitoring so that changes to rate-limiting rules or schema indexes on Cockroachdb trigger reviews before deployment. The GitHub Action can fail a build if risk scores exceed your defined threshold, ensuring that configuration drift does not weaken spraying defenses.