Password Spraying in Adonisjs with Basic Auth
Password Spraying in Adonisjs with Basic Auth — how this specific combination creates or exposes the vulnerability
Password spraying is an attack technique where an adversary uses a small number of common or compromised passwords against many usernames to avoid account lockouts. When an AdonisJS application exposes authentication via HTTP Basic Auth without additional protections, the endpoint becomes an ideal target for spraying because the protocol prompts the client to send a Base64-encoded username:password pair on every request. Because the application typically reveals whether a username exists through timing differences or distinct HTTP status codes (e.g., 401 vs 403), an attacker can iterate over user accounts while trying a single password such as Password123 or Welcome2024. MiddleBrick detects this behavior as part of its Authentication and BOLA/IDOR checks, highlighting the risk of credential validation logic that does not enforce uniform response times or rate limits.
In AdonisJS, Basic Auth is often implemented in route middleware or within an authentication provider. If the implementation performs an asynchronous user lookup and then compares passwords in a non-constant time manner, it can leak account existence and weaken password policy enforcement. For example, an endpoint that calls User.findBy('email', email) and then uses hash.verify(password, user.password) will behave differently depending on whether the user exists, allowing an attacker to map valid accounts across a spraying campaign. Furthermore, if the application does not enforce global rate limits on the authentication path, a spray can be executed rapidly by cycling through usernames while keeping each password attempt below per-IP or per-account thresholds. The scanner tests for weak or missing rate limiting as part of its Rate Limiting check, which is critical to detecting an exposed authentication surface suitable for password spraying.
Additionally, when combined with an OpenAPI/Swagger spec that documents the Basic Auth flow without clarifying security schemes or threat models, developers may inadvertently expose debug or production routes to unauthenticated probing. MiddleBrick’s spec analysis resolves $ref definitions and cross-references them with runtime behavior, identifying discrepancies such as unauthenticated paths that should require credentials. This is important because an API that accepts Basic Auth over non-TLS channels will leak credentials in clear text, compounding the spraying risk. The LLM/AI Security checks further ensure that no system prompt or configuration details are leaked through error messages or help text, which could aid an attacker in crafting targeted sprays. Overall, the combination of predictable user enumeration, weak rate controls, and improper handling of authentication errors creates a favorable environment for password spraying against AdonisJS services using Basic Auth.
Basic Auth-Specific Remediation in Adonisjs — concrete code fixes
To mitigate password spraying in AdonisJS, implement constant-time authentication checks, enforce strong per-account rate limits, and avoid revealing user existence through distinct responses. Below are concrete code examples that demonstrate a hardened approach using HTTP Basic Auth.
Example 1: Constant-time user lookup and password verification
Use a fixed hash for non-existent users to ensure response times do not reveal account validity. Install the @adonisjs/hash package if not already present, and create an AuthHelper utility.
// start/hashProvider.ts or utils/authHelper.ts
import { Hash } from '@ioc:Adonis/Core/Hash'
export class AuthHelper {
public static async verifyPassword(inputPassword: string, storedHash: string): Promise {
return Hash.verify(storedHash, inputPassword)
}
public static async dummyVerify(): Promise {
// Use a pre-generated hash that is computationally similar to a real bcrypt hash
return Hash.verify('$2a$10$abcdefghijklmnopqrstuDummyHashForTimingConsistency123456', 'dummy')
}
}
In your login handler, always fetch a user row by a normalized identifier (e.g., email), then verify against a dummy hash when the user is not found.
// controllers/AuthController.ts
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import User from 'App/Models/User'
import { AuthHelper } from 'App/Helpers/AuthHelper'
export default class AuthController {
public async basicAuthLogin({ request, auth, response }: HttpContextContract) {
const { username, password } = request.auth()
// Normalize input to avoid timing leaks via string comparison short-circuit
const normalizedUsername = String(username || '').trim().toLowerCase()
// Always fetch a record; if missing, fall back to a dummy user
let user = await User.findBy('email', normalizedUsername)
if (!user) {
// Create a placeholder to ensure constant-time verification path
user = new User()
user.email = normalizedUsername
user.password = await AuthHelper.dummyVerify().then(() => '$2a$10$abcdefghijklmnopqrstuDummyHashForTimingConsistency123456')
}
const isValid = await AuthHelper.verifyPassword(password, user.password)
if (!isValid) {
return response.unauthorized({ message: 'Invalid credentials' })
}
// Generate session or token as appropriate
const token = await auth.generate(user)
return response.ok({ token })
}
}
Example 2: Global rate limiting on the authentication route
Apply route-specific rate limits to throttle requests per IP or per normalized username. AdonisJS provides a built-in rate limiter that can be attached to routes in the start/routes.ts.
// start/routes.ts
import Route from '@ioc:Adonis/Core/Route'
Route.post('auth/login', 'AuthController.basicAuthLogin')
.rateLimit({
duration: 60, // window in seconds
max: 10, // max requests per window
identifier: (ctx) => {
// Use IP + normalized username for tighter control during spraying
const body = ctx.request.body()
const username = String(body.username || '').trim().toLowerCase()
return ctx.request.ip + '|' + username
}
})
Example 3: Enforce TLS and secure headers
Ensure Basic Auth is only accepted over HTTPS and that sensitive headers are not exposed in logs or errors.
// start/kernel.ts or middleware
import { ExceptionHandler } from '@ioc:Adonis/Core/ExceptionHandler'
export default class Kernel {
public async handle(error: any, ctx: HttpContextContract) {
if (error.name === 'InvalidCredentialsError') {
// Avoid detailed messages that aid attackers
ctx.response.status(401).send({ error: 'Unauthorized' })
return
}
return super.handle(error, ctx)
}
}
Combine these measures with continuous scanning using MiddleBrick’s CLI or GitHub Action to validate that your remediation does not reintroduce authentication or rate-limiting gaps. The dashboard can track security scores over time, while the MCP Server enables quick scans from your AI coding assistant during development.