HIGH password sprayingadonisjsbasic auth

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.

Frequently Asked Questions

Why does Basic Auth over AdonisJS expose endpoints to password spraying?
Basic Auth transmits credentials on every request and often reveals user existence through timing or status-code differences. Without constant-time verification and strict rate limiting, attackers can systematically test passwords across many accounts, which is detectable by MiddleBrick’s Authentication and Rate Limiting checks.
How can I validate that my remediation works using middleBrick?
Run middlebrick scan against your endpoint after implementing constant-time checks and rate limits. Use the CLI for iterative testing and the GitHub Action to fail builds if the authentication risk score degrades, ensuring your changes reduce the spray surface.