HIGH race conditionadonisjsbasic auth

Race Condition in Adonisjs with Basic Auth

Race Condition in Adonisjs with Basic Auth — how this specific combination creates or exposes the vulnerability

A race condition in AdonisJS when using Basic Auth arises from the interaction between concurrent requests, shared state, and the way authentication credentials are validated per request. In AdonisJS, HTTP authentication is typically handled in route or middleware layers using the built-in auth module. When Basic Auth is used, the username and password are sent in the Authorization header on every request. The vulnerability surface appears when multiple requests that depend on shared resources (such as a database row or an in-memory counter) are processed concurrently without proper synchronization.

Consider a scenario where an endpoint performs a read-modify-write sequence: it reads a resource, checks user permissions, and updates it. If two authenticated requests execute these steps in an interleaved manner, the final state may reflect an inconsistent or unintended update. For example, two requests with valid Basic Auth credentials could both read the same version of a resource, apply updates based on that stale read, and then write back, causing one update to be lost. This is a classic time-of-check-to-time-of-use (TOCTOU) pattern enabled by concurrency and insufficient isolation between operations.

AdonisJS does not inherently serialize requests for a given user or resource; each request is handled independently by the Node.js runtime. If the application logic does not implement idempotency, locking, or versioning (such as an ETag or optimistic lock), an attacker who can issue rapid, authenticated requests may exploit the window between check and write. In the context of Basic Auth, this risk is not due to the transmission format but due to the application’s handling of shared state across authenticated sessions. The presence of Basic Auth identifies the user, but if the endpoint does not ensure that operations are atomic or isolated, authenticated concurrency can lead to data corruption or privilege escalation-like effects when combined with authorization checks that also suffer from BOLA/IDOR issues.

Real-world examples include endpoints that increment a counter or adjust a balance without atomic operations, or endpoints that modify permissions based on prior reads. In such cases, an authenticated attacker could craft parallel requests to observe or influence outcomes, effectively bypassing intended logical constraints. Since middleBrick tests unauthenticated attack surfaces, it can detect endpoints where authentication is weak or where concurrency-related authorization flaws exist through parallel probe requests and spec/runtime analysis, highlighting endpoints that lack proper safeguards despite correct Basic Auth usage.

Basic Auth-Specific Remediation in Adonisjs — concrete code fixes

To mitigate race conditions in AdonisJS while using Basic Auth, focus on making each authenticated operation atomic, isolated, and idempotent. Use database-level constraints and transactions to enforce consistency, and avoid relying on in-memory state or non-atomic read-update-write flows. Below are concrete patterns and code examples.

1. Use database transactions with row locking

When updating shared resources, wrap operations in a transaction and lock the relevant row to prevent concurrent modifications. In AdonisJS with Lucid ORM, you can use trx and forUpdate to lock rows.

import Database from '@ioc:Adonis/Lucid/Database'
import User from 'App/Models/User'

export default class UsersController {
  public async updateBalance({ request, auth }) {
    const user = auth.user! // Basic Auth ensures user is set
    const amount = request.input('amount')

    const trx = await Database.transaction()
    try {
      // Lock the row for update within the transaction
      const lockedUser = await User.query(trx).where('id', user.id).forUpdate().firstOrFail()
      lockedUser.balance = lockedUser.balance + amount
      await lockedUser.save(trx)
      await trx.commit()
    } catch (error) {
      await trx.rollback()
      throw error
    }
  }
}

2. Apply optimistic concurrency control with version numbers

Add a version column to your resource and check it on each update. If the version mismatches, the update fails, and the client can retry. This avoids overwriting concurrent changes.

import User from 'App/Models/User'

export default class UsersController {
  public async updateProfile({ request, auth, response }) {
    const user = auth.user!
    const incomingVersion = request.input('version')

    if (user.version !== incomingVersion) {
      return response.badRequest({ error: 'Conflict: resource modified by another request' })
    }

    user.merge({ name: request.input('name'), version: user.version + 1 })
    await user.save()
  }
}

3. Use atomic database operations where possible

Instead of read-then-write, prefer atomic increments/decrements directly in the database. This removes the race window entirely.

import User from 'App/Models/User'

export default class UsersController {
  public async incrementClicks({ request, auth }) {
    const user = auth.user!
    await User.query().where('id', user.id).increment('clicks', 1)
  }
}

4. Ensure idempotency for critical endpoints

For operations that should be safe to retry, use client-provided idempotency keys stored server-side to deduplicate requests within a short window.

import IdempotencyKey from 'App/Models/IdempotencyKey'

export default class PaymentsController {
  public async store({ request, auth, response }) {
    const user = auth.user!
    const key = request.input('idempotency_key')
    const existing = await IdempotencyKey.findBy('key', key)

    if (existing) {
      return response.ok(existing.result)
    }

    // Perform the operation atomically with the key stored alongside
    const result = { status: 'processed' }
    await IdempotencyKey.create({ key, user_id: user.id, result })
    return response.created(result)
  }
}

5. Apply rate limiting and request validation

Even with atomic updates, limit excessive authenticated requests that could probe for race conditions. Combine route-level middleware with validation schemas to reject malformed or suspicious concurrent patterns.

import { schema } from '@ioc:Adonis/Core/Validator'
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import Route from '@ioc:Adonis/Core/Route'

Route.post('/account/update', async ({ request }: HttpContextContract) => {
  const bodySchema = schema.create({
    amount: schema.number([schema.unsigned(), schema.min(1)]),
    idempotencyKey: schema.string.optional()
  })
  await request.validate({ schema: bodySchema })
}).middleware(['auth:basic', 'throttle:rate-limit'])

By combining database transactions, optimistic locking, atomic operations, idempotency, and rate limiting, you reduce the window and impact of race conditions for endpoints protected by Basic Auth in AdonisJS.

Frequently Asked Questions

Does Basic Auth itself introduce a race condition in AdonisJS?
Basic Auth does not inherently introduce a race condition. The risk comes from how the application uses shared resources and handles concurrent authenticated requests. Proper synchronization and atomic patterns mitigate the issue.
Can middleBrick detect race conditions in AdonisJS APIs using Basic Auth?
middleBrick tests unauthenticated attack surfaces and can identify endpoints where authentication and concurrency may lead to inconsistent behavior through parallel probes and spec/runtime cross-references, but it does not fix the condition.