HIGH replay attackadonisjsapi keys

Replay Attack in Adonisjs with Api Keys

Replay Attack in Adonisjs with Api Keys — how this specific combination creates or exposes the vulnerability

A replay attack in AdonisJS when using API keys occurs when an attacker intercepts a valid request— including the API key in headers — and re-sends it to the API to perform the same action again. Because API keys are often static credentials issued to a client and sometimes used without additional context, they do not inherently prevent replays unless the API adds nonces, timestamps, or per-request signatures.

Consider an AdonisJS route that relies only on an API key for authorization:

// start/hooks.ts
import { defineConfig } from '@adonisjs/core'
import apiKey from '@ioc:Adonisjs/ApiKey' // hypothetical provider

export default defineConfig({
  http: {
    middleware: ['apiKeyAuth']
  }
})

// middleware/api_key_auth.ts
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { ApiKeyProvider } from '@ioc:Adonisjs/ApiKey'

export default async function apiKeyAuth({ request, response, auth }: HttpContextContract, next: () => Promise) {
  const key = request.header('X-API-Key')
  if (!key) {
    return response.unauthorized('API key missing')
  }
  const isValid = await ApiKeyProvider.validate(key)
  if (!isValid) {
    return response.forbidden('Invalid API key')
  }
  await next()
}

If this endpoint is idempotent (e.g., GET /balance or POST /transfer), an attacker who captures the request with a valid API key can replay it to drain funds or leak sensitive data. API keys alone do not bind the request to a timestamp, client IP, or a one-time nonce, so the same key and payload can be reused.

AdonisJS applications often expose endpoints that accept JSON payloads without additional per-request context. For example:

// controllers/TransactionController.ts
import { schema } from '@ioc:Adonis/Core/Validator'
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'

const transactionSchema = schema.create({
  amount: schema.number([min(0.01)]),
  toAccount: schema.string()
})

export default class TransactionsController {
  public async store({ request, response }: HttpContextContract) {
    const payload = request.validate({ schema: transactionSchema })
    // process transaction — vulnerable to replay because API key validated only once
    return response.created({ ok: true })
  }
}

Without a nonce or timestamp verified server-side, an intercepted POST with { amount: 1000, toAccount: 'attacker' } can be replayed multiple times. MiddleBrick detects such patterns by correlating repeated requests with the same API key and payload characteristics across unauthenticated scans, highlighting the risk of missing replay protections.

Moreover, if API keys are transmitted over unencrypted channels (or if encryption is not enforced consistently), interception risk increases. Even with HTTPS, lack of application-level replay defenses means captured requests remain useful to an attacker. The scan checks whether endpoints using API keys also implement timestamp or nonce validation and flags those that do not.

Api Keys-Specific Remediation in Adonisjs — concrete code fixes

To mitigate replay attacks while continuing to use API keys in AdonisJS, introduce request-level uniqueness and server-side verification. Below are concrete, working examples that you can adopt.

1. Add timestamp and nonce validation to the middleware

Require clients to send an X-Request-Timestamp and X-Nonce header. The server checks freshness (e.g., within 2 minutes) and uniqueness (e.g., store recent nonces for a window).

// middleware/replay_protection.ts
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import crypto from 'node:crypto'

// Simple in-memory store for demo; use Redis or database in production
const recentNonces = new Set()

export default async function replayProtection({ request, response }: HttpContextContract, next: () => Promise) {
  const timestamp = request.header('X-Request-Timestamp')
  const nonce = request.header('X-Nonce')
  const apiKey = request.header('X-API-Key')

  if (!timestamp || !nonce || !apiKey) {
    return response.badRequest('Missing replay protection headers')
  }

  const now = Date.now()
  const reqTime = parseInt(timestamp, 10)
  if (isNaN(reqTime) || Math.abs(now - reqTime) > 120_000) {
    return response.unauthorized('Request expired')
  }

  if (recentNonces.has(nonce)) {
    return response.unauthorized('Replay detected')
  }

  // Optionally bind nonce to API key to prevent cross-key replays
  const bindingKey = `${apiKey}:${nonce}`
  if (recentNonces.has(bindingKey)) {
    return response.unauthorized('Replay detected')
  }

  recentNonces.add(bindingKey)
  // prune old nonces periodically in production
  await next()
}

Use this middleware before your API key validation so replay checks happen early.

2. Require client signatures for critical operations

For state-changing endpoints, have the client sign the payload with a shared secret. The server recomputes the signature and rejects mismatches.

// utils/signature.ts
import crypto from 'node:crypto'

export function generateSignature(payload: object, secret: string): string {
  const body = JSON.stringify(payload)
  return crypto.createHmac('sha256', secret).update(body).digest('hex')
}

// In the route handler
import { signature } from '@ioc:Adonisjs/Crypto' // example
// or use the utils above

// Example route combining API key and signature
export default class TransactionsController {
  public async store({ request, response }: HttpContextContract) {
    const apiKey = request.header('X-API-Key')
    const signature = request.header('X-Signature')
    const payload = request.only(['amount', 'toAccount'])

    // validate apiKey as before
    const isValidKey = await ApiKeyProvider.validate(apiKey)
    if (!isValidKey) {
      return response.forbidden('Invalid API key')
    }

    // compute expected signature using stored secret per key
    const secret = await ApiKeyProvider.getSharedSecret(apiKey)
    const expected = generateSignature(payload, secret)

    if (signature !== expected) {
      return response.unauthorized('Invalid signature')
    }

    // process transaction — protected against replay if nonce/timestamp also used
    return response.created({ ok: true })
  }
}

3. Enforce idempotency keys for POST/PUT/DELETE

Require an Idempotency-Key header for mutating requests and track processed keys with TTL to prevent duplicate processing.

// middleware/idempotency.ts
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'

const processedKeys = new Set()

export default async function idempotency({ request, response }: HttpContextContract, next: () => Promise) {
  if (['POST', 'PUT', 'DELETE'].includes(request.method())) {
    const idKey = request.header('Idempotency-Key')
    if (!idKey) {
      return response.badRequest('Idempotency-Key required')
    }
    if (processedKeys.has(idKey)) {
      return response.conflict('Duplicate request')
    }
    processedKeys.add(idKey)
    // schedule cleanup for idKey after appropriate TTL
  }
  await next()
}

Combine these techniques—API key validation plus timestamp/nonce or signatures—to reduce the likelihood of replay attacks. MiddleBrick’s scans can verify whether endpoints using API keys include these protections and surface findings with severity and remediation guidance.

Frequently Asked Questions

What headers should an AdonisJS client send to prevent replay attacks when using API keys?
Send X-API-Key, X-Request-Timestamp (current epoch ms), X-Nonce (unique per request), and for critical endpoints X-Signature (HMAC of the payload with the key’s shared secret). Optionally include Idempotency-Key for mutating methods.
Can MiddleBrick detect replay vulnerabilities in API key–protected AdonisJS endpoints?
Yes. MiddleBrick scans unauthenticated attack surfaces and flags endpoints that use static API keys without replay protections such as timestamp/nonce validation or signatures, providing severity and remediation guidance.