Replay Attack in Adonisjs with Cockroachdb
Replay Attack in Adonisjs with Cockroachdb — how this specific combination creates or exposes the vulnerability
A replay attack in the context of Adonisjs communicating with Cockroachdb occurs when an attacker intercepts a valid request—such as an authenticated API call or a database query—and re-sends it at a later time to achieve unauthorized effects. Because Adonisjs applications often rely on stateless tokens (e.g., JWT) or session identifiers, and Cockroachdb serves as the distributed SQL backend, the risk centers around idempotent operations and lack of per-request uniqueness.
Specific to this stack, replay exposure commonly arises when endpoints that mutate state (POST/PUT/DELETE) do not enforce strict request uniqueness. For example, if an Adonisjs controller processes a payment or state change by issuing SQL against Cockroachdb without a nonce or timestamp, an attacker can capture a valid HTTP payload and replay it, causing duplicate transactions or unexpected state changes. Cockroachdb’s strong consistency guarantees mean that repeated, valid-looking writes are applied deterministically, which can amplify the impact of a replayed request.
Another vector involves token or session replay. If Adonisjs issues a JWT with a long lifetime and does not maintain server-side revocation state, an attacker can reuse the token to repeatedly access Cockroachdb-backed resources. Since Cockroachdb does not inherently understand application-level tokens, the database will honor requests that present valid credentials or connection parameters, making server-side enforcement in Adonisjs essential.
Additionally, without idempotency keys or request identifiers, retry logic in clients (e.g., HTTP libraries or ORM query retries) can inadvertently enable replay behavior. Adonisjs applications that use services like Database from @ioc:Adonis/Lucid may see retries on network glitches; if those retries are not guarded, the same mutation is applied multiple times against Cockroachdb.
To detect such issues, middleBrick scans an Adonisjs endpoint set against a Cockroachdb instance in black-box mode, exercising unauthenticated and authenticated paths to uncover missing nonce checks, absent idempotency mechanisms, and overly permissive token lifetimes. Findings include severity-ranked guidance on introducing request identifiers, short-lived tokens, and server-side replay detection.
Cockroachdb-Specific Remediation in Adonisjs — concrete code fixes
Remediation focuses on ensuring each request to Adonisjs is unique and verifiable before it reaches Cockroachdb, and on tightening token/session handling. Below are concrete, idiomatic Adonisjs code examples that integrate with Cockroachdb safely.
1. Idempotency key with UUID and database check
Require an Idempotency-Key header on mutating requests and store processed keys in Cockroachdb to reject duplicates.
import { DateTime } from 'luxon'
import { schema } from '@ioc:Adonis/Core/Validator'
import Database from '@ioc:Adonis/Lucid/Database'
export default class IdempotencyController {
public async store({ request, response }) {
const keySchema = schema.create({
idempotencyKey: schema.string.optional(),
payload: schema.object({}),
})
const { idempotencyKey, payload } = await request.validate({ schema: keySchema })
const key = idempotencyKey || request.uuid()
// Check if key already processed in Cockroachdb
const existing = await Database.from('idempotency_keys').where('key', key).limit(1).first()
if (existing) {
return response.status(200).json({ cached: true, result: existing.response })
}
// Process the request (example: insert into a transactions table)
const tx = await Database.transaction(async (trx) => {
const result = await trx
.from('transactions')
.insert({ data: payload, createdAt: DateTime.local().toISO() })
// Record idempotency key
await trx.from('idempotency_keys').insert({
key,
response: result,
expiresAt: DateTime.local().plus({ hours: 24 }).toISO(),
})
return result
})
return response.created({ transaction: tx })
}
}
2. Short-lived JWTs and refresh token rotation
Issue short-lived access tokens and use rotating refresh tokens stored server-side (e.g., in Cockroachdb) to prevent token reuse.
import { BaseProvider, DatabaseProvider } from '@ioc:Adonis/Addons/Auth'
import Database from '@ioc:Adonis/Lucid/Database'
import { jwtVerify } from 'jose'
export class CustomSessionProvider extends BaseProvider {
public async verify(token: string) {
const { payload, protectedHeader } = await jwtVerify(token, new TextEncoder().encode(process.env.JWT_SECRET))
const stored = await Database.from('refresh_tokens')
.where('jti', payload.jti)
.where('expires_at', '>', DateTime.local().toSQLDateTime())
.first()
if (!stored || stored.revoked) {
throw new Error('Invalid or revoked token')
}
return { user_id: payload.sub, session_id: stored.id }
}
}
3. Request timestamp and nonce validation
Require a recent timestamp and a one-time nonce to mitigate replays within the Adonisjs middleware layer.
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import Database from '@ioc:Adonis/Lucid/Database'
export default async function replayProtection(ctx: HttpContextContract) {
const timestamp = ctx.request.header('x-timestamp')
const nonce = ctx.request.header('x-nonce')
if (!timestamp || !nonce) {
ctx.response.badRequest({ error: 'Missing replay protection headers' })
return false
}
const windowMs = 5 * 60 * 1000 // 5 minutes
const now = Date.now()
if (Math.abs(now - parseInt(timestamp, 10)) > windowMs) {
ctx.response.status(400).json({ error: 'Stale request' })
return false
}
const seen = await Database.from('nonces').where('nonce', nonce).limit(1).first()
if (seen) {
ctx.response.status(400).json({ error: 'Replay detected' })
return false
}
await Database.table('nonces').insert({ nonce, expiresAt: new Date(now + windowMs) })
return true
}
4. Cockroachdb schema considerations
Define tables that support replay detection with appropriate TTLs to avoid unbounded growth.
-- idempotency_keys table
CREATE TABLE idempotency_keys (
key STRING PRIMARY KEY,
response JSONB,
expires_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX ON idempotency_keys (expires_at);
-- nonces table with TTL via Cockroachdb's row-level TTL
CREATE TABLE nonces (
nonce STRING PRIMARY KEY,
expires_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX ON nonces (expires_at);
5. Middleware integration in Adonisjs routes
Apply the replay protection middleware to sensitive routes in your router file.
import Route from '@ioc:Adonis/Core/Route'
import replayProtection from 'App/Middleware/replay_protection'
Route.post('/payment', 'PaymentController.store').middleware([replayProtection])
Route.delete('/account/:id', 'AccountController.destroy').middleware([replayProtection])