HIGH padding oracleadonisjsmutual tls

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' })
  }
})

Frequently Asked Questions

Does Mutual Tls prevent padding oracle attacks?
No. Mutual Tls ensures client authentication but does not affect how the application decrypts data. Padding oracles depend on decryption behavior, so use authenticated encryption and uniform error handling regardless of TLS client authentication.
What should I do if I must use CBC mode in Adonisjs?
Always use encrypt-then-MAC with a strong HMAC, verify the MAC before any padding removal, and return a generic error for any failure. Avoid returning distinct errors for padding versus integrity failures, and prefer AES-GCM where possible.