Replay Attack in Adonisjs with Mutual Tls
Replay Attack in Adonisjs with Mutual Tls — how this specific combination creates or exposes the vulnerability
A replay attack occurs when an adversary captures a valid request—complete with headers, payload, and TLS metadata—and retransmits it to the server to produce an unauthorized effect. In AdonisJS, enabling Mutual TLS (mTLS) ensures that both client and server present certificates, establishing strong authentication at the transport layer. However, mTLS alone does not prevent replay because it validates identity, not the freshness or uniqueness of the request. An attacker can still record a properly authenticated TLS session and replay it later, and the server will accept it because the client certificate is valid and the request structure is unchanged.
When mTLS is configured in AdonisJS, the framework relies on the underlying Node.js TLS layer to verify client certificates. If the application does not enforce additional protections—such as nonces, timestamps, or unique request identifiers—there is a gap between transport-layer assurance and application-layer semantics. For example, an authenticated POST to /api/transfer with an idempotent-looking payload can be replayed within the certificate’s validity window, potentially leading to duplicate transactions or privilege escalation if the endpoint does not incorporate idempotency controls.
Moreover, certain server-side behaviors can inadvertently aid replay. If logs or monitoring inadvertently capture request bodies that include sensitive identifiers, and those logs are exposed, an attacker can harvest data to reconstruct requests. Additionally, if mTLS is not coupled with strict cipher suites that provide forward secrecy, a compromised server key could allow retrospective decryption of captured traffic, further exposing request patterns. Therefore, while mTLS raises the bar for impersonation, it must be augmented with application-level defenses to be effective against replay in AdonisJS.
Mutual Tls-Specific Remediation in Adonisjs — concrete code fixes
To mitigate replay in AdonisJS with Mutual TLS, combine robust mTLS setup with anti-replay mechanisms such as timestamp windows and nonce tracking. Below are concrete, syntactically correct examples that integrate into an AdonisJS project using the native @adonisjs/ssl-server package or custom HTTPS server creation.
1. Enabling Mutual TLS in AdonisJS (server-side)
Configure the HTTPS server to require client certificates and validate them against a trusted CA. This ensures only authorized clients can establish TLS sessions, providing the baseline for mTLS.
// start/server.ts
import { Application } from '@adonisjs/core/types'
import { createServer } from 'node:https'
import { readFileSync } from 'node:fs'
export default class ServerProvider {
public async register() {
// This example assumes you are customizing the server creation
// within a provider or custom command.
}
public async boot() {
const httpsOptions = {
key: readFileSync('path/to/server.key'),
cert: readFileSync('path/to/server.crt'),
ca: readFileSync('path/to/ca.crt'),
requestCert: true,
rejectUnauthorized: true,
}
const server = createServer(httpsOptions, (req, res) => {
// req.client.verified will be true if client cert verified
if (!req.client.verified) {
res.statusCode = 403
res.end('Client certificate required')
return
}
// Route handling
})
server.listen(443, () => {
console.log('mTLS-enabled server listening on :443')
})
}
}
2. Client-side mTLS request with nonced timestamp
Clients should include a timestamp and a nonce in headers, and sign a canonical representation of the request to defend against replay. The server verifies freshness and uniqueness.
// Example client request in an AdonisJS service (e.g., using axios)
import axios from 'axios'
import crypto from 'node:crypto'
const generateNonce = (): string => crypto.randomBytes(16).toString('hex')
const generateTimestamp = (): string => Math.floor(Date.now() / 1000).toString()
const apiClient = axios.create({
httpsAgent: new (require('https').Agent)({
cert: 'path/to/client.crt',
key: 'path/to/client.key',
ca: 'path/to/ca.crt',
}),
})
const callProtectedEndpoint = async (url: string, payload: object) => {
const nonce = generateNonce()
const timestamp = generateTimestamp()
const canonical = JSON.stringify({ method: 'POST', url, payload, nonce, timestamp })
const signature = crypto.createHmac('sha256', Buffer.from(process.env.REQUEST_SECRET!)).update(canonical).digest('hex')
const response = await apiClient.post(url, payload, {
headers: {
'X-Request-Nonce': nonce,
'X-Request-Timestamp': timestamp,
'X-Request-Signature': signature,
'Content-Type': 'application/json',
},
})
return response.data
}
3. Server-side replay protection in AdonisJS middleware
Implement middleware that checks timestamp freshness (e.g., within 2 minutes) and maintains a short-lived cache of nonces to reject duplicates. This works alongside mTLS client authentication to close the replay gap.
// middleware/replay_protection.ts
import { HttpContextContract } from '@adonisjs/core/http'
import { create as createRedisClient } from 'redis'
const redis = createRedisClient({ url: process.env.REDIS_URL })
redis.connect().catch(console.error)
const REPLAY_WINDOW_SECONDS = 120
export default class ReplayProtectionMiddleware {
public async handle({ request, response, logger }: HttpContextContract, next: () => Promise) {
const nonce = request.header('X-Request-Nonce')
const timestampHeader = request.header('X-Request-Timestamp')
const signature = request.header('X-Request-Signature')
if (!nonce || !timestampHeader || !signature) {
return response.status(400).send({ error: 'Missing replay protection headers' })
}
const timestamp = parseInt(timestampHeader, 10)
const now = Math.floor(Date.now() / 1000)
if (Math.abs(now - timestamp) > REPLAY_WINDOW_SECONDS) {
logger.warn({ nonce }, 'Stale request rejected')
return response.status(400).send({ error: 'Request timestamp out of window' })
}
const cacheKey = `nonce:${nonce}`
const existing = await redis.get(cacheKey)
if (existing === '1') {
logger.warn({ nonce }, 'Replay detected')
return response.status(400).send({ error: 'Duplicate request rejected' })
}
// Optional: verify HMAC signature using shared secret
const canonical = JSON.stringify({ method: request.method(), url: request.url(), body: request.body(), nonce, timestamp })
const expected = crypto.createHmac('sha256', Buffer.from(process.env.REQUEST_SECRET!)).update(canonical).digest('hex')
if (signature !== expected) {
logger.warn({ nonce }, 'Signature mismatch')
return response.status(401).send({ error: 'Invalid signature' })
}
await redis.set(cacheKey, '1', { EX: REPLAY_WINDOW_SECONDS })
await next()
}
}
By combining mTLS with nonce-based replay protection, AdonisJS applications can accept only fresh, authenticated requests, significantly reducing the risk of replay even when TLS sessions are captured.