Padding Oracle in Adonisjs with Mutual Tls
Padding Oracle in Adonisjs with Mutual Tls — how this specific combination creates or exposes the vulnerability
A padding oracle attack occurs when an application reveals information about the validity of a padding block during decryption. In Adonisjs, this typically arises when using symmetric encryption (e.g., AES-CBC) and returning distinct errors for invalid padding versus invalid authentication. When Mutual TLS is in use, the server validates the client certificate before processing the request, but the encryption layer can still be reached over the mutually authenticated TLS channel. If the application then decrypts user-controlled ciphertext and responds with different error messages or timing for padding errors versus other failures, the TLS-secured channel does not prevent the oracle: an attacker who has obtained a valid client certificate (or is using a compromised or malicious certificate) can iteratively submit manipulated ciphertexts and observe responses to infer plaintext.
With Mutual Tls, the client presents a certificate and the server verifies it via trusted CA, client certificate verification, and potentially revocation checks. This ensures the identity of the caller but does not alter how the application handles decryption. If Adonisjs code uses a low-level crypto primitive such as crypto.createDecipheriv and does not use an authenticated encryption mode (e.g., AES-GCM), the decrypt step will first remove PKCS7 padding and then verify integrity (if a MAC is used separately). A mismatch in padding bytes can throw an early error, while a valid padding but bad MAC may proceed further before failing, creating a distinguishable behavior. An attacker can leverage this by observing HTTP status codes, response bodies, or timing differences to conduct a padding oracle attack even though TLS client authentication is enforced.
In practice, this risk is realized when an endpoint accepts encrypted payloads (e.g., a webhook or API body) and decrypts them using a static key managed within the Adonisjs app. Mutual Tls limits who can reach the endpoint, but if the endpoint logic is vulnerable, a malicious certificate holder can still perform the oracle. Additionally, if the server returns stack traces or verbose errors in development mode, the leakage is more severe. The combination therefore exposes a classic cryptography flaw despite strong transport-layer identity assurance.
Mutual Tls-Specific Remediation in Adonisjs — concrete code fixes
Remediation focuses on ensuring decryption never distinguishes padding errors and using authenticated encryption. Below are concrete Adonisjs examples with Mutual Tls setup and secure decryption.
1. Mutual Tls setup in Adonisjs (server-side)
Configure the HTTPS server to require and verify client certificates:
// start/server.ts
import { defineConfig } from '@adonisjs/core/app'
export default defineConfig({
https: {
key: 'path/to/server-key.pem',
cert: 'path/to/server-cert.pem',
ca: 'path/to/ca-chain.pem',
requestCert: true,
rejectUnauthorized: true,
},
})
Ensure your Node.js HTTPS server (used by Adonisjs) enforces requestCert: true and rejectUnauthorized: true. This makes the TLS layer reject connections without a valid, trusted client certificate.
2. Secure decryption using authenticated encryption
Replace manual padding-based ciphers with AES-GCM, which provides confidentiality and integrity in one step and does not require padding:
// app/Helpers/crypto.ts
import crypto from 'crypto'
const ALGORITHM = 'aes-256-gcm'
const KEY = Buffer.from(process.env.ENCRYPTION_KEY!, 'hex')
export function encrypt(text: string): string {
const iv = crypto.randomBytes(12)
const cipher = crypto.createCipheriv(ALGORITHM, KEY, iv)
const encrypted = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()])
const tag = cipher.getAuthTag()
// store iv, tag, encrypted payload together (e.g., concatenated or JSON)
return Buffer.concat([iv, tag, encrypted]).toString('base64')
}
export function decrypt(dataB64: string): string {
const combined = Buffer.from(dataB64, 'base64')
const iv = combined.subarray(0, 12)
const tag = combined.subarray(12, 28)
const ciphertext = combined.subarray(28)
const decipher = crypto.createDecipheriv(ALGORITHM, KEY, iv)
decipher.setAuthTag(tag)
try {
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8')
return plaintext
} catch (err) {
// Use a constant-time comparison or simply return a generic error to avoid padding/oracle leaks
throw new Error('decryption failed')
}
}
If you must use CBC (not recommended), always use an HMAC in an encrypt-then-MAC pattern and verify the HMAC before attempting to remove padding, ensuring errors are uniform:
// app/Helpers/crypto-cbc-hmac.ts
import crypto from 'crypto'
const ALGORITHM = 'aes-256-cbc'
const KEY = Buffer.from(process.env.ENCRYPTION_KEY!, 'hex')
export function encryptCbcWithHmac(text: string): string {
const iv = crypto.randomBytes(16)
const cipher = crypto.createCipheriv(ALGORITHM, KEY, iv)
const ciphertext = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()])
const hmac = crypto.createHmac('sha256', KEY).update(iv).update(ciphertext).digest()
return Buffer.concat([iv, ciphertext, hmac]).toString('base64')
}
export function decryptCbcWithHmac(dataB64: string): string {
const combined = Buffer.from(dataB64, 'base64')
const iv = combined.subarray(0, 16)
const ciphertext = combined.subarray(16, combined.length - 32)
const receivedHmac = combined.subarray(combined.length - 32)
const computedHmac = crypto.createHmac('sha256', KEY).update(iv).update(ciphertext).digest()
if (!crypto.timingSafeEqual(receivedHmac, computedHmac)) {
throw new Error('authentication failed')
}
const decipher = crypto.createDecipheriv(ALGORITHM, KEY, iv)
try {
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8')
return plaintext
} catch (err) {
throw new Error('decryption failed')
}
}
In both cases, ensure errors are caught and a generic message is returned, avoiding any distinction based on padding validity. Combined with Mutual Tls, this prevents an unauthenticated attacker from reaching the endpoint and an authenticated but malicious certificate holder from learning padding information.
3. Endpoint handling in Adonisjs
Use a consistent error response and avoid leaking details:
// routes.ts
import Route from '@adonisjs/core/http/router'
import { decrypt, encrypt } from '~App/Helpers/crypto'
Route.post('/secure', async ({ request, response }) => {
const payload = request.input('data')
try {
const decrypted = decrypt(payload)
// process decrypted data
response.json({ ok: true, data: 'processed' })
} catch (err) {
response.status(400).json({ error: 'invalid request' })
}
})