Padding Oracle in Adonisjs with Dynamodb
Padding Oracle in Adonisjs with Dynamodb — how this specific combination creates or exposes the vulnerability
A padding oracle attack occurs when an application reveals whether decrypted ciphertext has valid padding, allowing an attacker to iteratively decrypt messages without knowing the key. In Adonisjs, this typically arises when encrypted data (e.g., authentication tokens, session identifiers, or API keys) is stored in DynamoDB and later decrypted in application code. If decryption errors due to invalid padding are surfaced as distinct responses or logs, the attacker can use these side channels to recover plaintext.
Consider an Adonisjs service that stores an encrypted user session in DynamoDB. The item might include an attribute session_cipher containing an AES-encrypted value. When the application retrieves the item from DynamoDB and attempts to decrypt it using a function like crypto.privateDecrypt or a custom wrapper, an invalid PKCS7 padding throws an exception. If the HTTP response differs—such as a 400 Bad Request with a message like “Invalid padding”—versus a generic decryption failure, the distinction acts as an oracle.
DynamoDB itself does not introduce padding oracle vulnerabilities; it is a storage layer. However, the combination amplifies risk because:
- The encrypted payload is persisted in a structured database, enabling offline capture and repeated decryption attempts.
- Adonisjs routes that read from DynamoDB and perform decryption often handle high-value operations (authentication, authorization), making the oracle more attractive.
- Error handling patterns in JavaScript/Node.js can inadvertently expose padding validity through stack traces or distinct error messages, especially if exceptions are not uniformly caught and sanitized.
Real-world scenarios include an API endpoint like POST /session/restore that accepts an encrypted token, fetches associated metadata from DynamoDB, and decrypts it. If the endpoint returns { error: 'Invalid padding' } for malformed padding and { error: 'Decryption failed' } for other failures, an attacker can mount a padding oracle attack to decrypt the token, potentially leading to privilege escalation or session hijacking (e.g., CVE-2016-2183-like impacts when weak crypto hygiene intersects with storage).
To mitigate this in Adonisjs with DynamoDB, ensure decryption errors are handled uniformly, use authenticated encryption (e.g., AES-GCM), and avoid exposing low-level cryptographic exceptions to the client. Leverage the framework’thooks and middleware to centralize error responses so that padding-related failures are indistinguishable from generic failures.
Dynamodb-Specific Remediation in Adonisjs — concrete code fixes
Remediation focuses on preventing distinguishable error paths and using robust cryptographic primitives. Below are concrete patterns for Adonisjs applications that store and retrieve encrypted data in DynamoDB.
1. Use authenticated encryption (AES-GCM) instead of raw AES-CBC
Authenticated encryption provides integrity alongside confidentiality, eliminating padding oracle concerns. In Adonisjs, use the built-in crypto module with GCM mode and store the authentication tag alongside the ciphertext in DynamoDB.
import { randomBytes, createCipheriv, createDecipheriv } from 'node:crypto';
export class SecureDynamoDBService {
private key: Buffer; // 32-byte AES-256 key managed via environment/vault
constructor() {
this.key = Buffer.from(process.env.ENCRYPTION_KEY!, 'base64');
}
encrypt(plaintext: string) {
const iv = randomBytes(12); // GCM recommended IV length
const cipher = createCipheriv('aes-256-gcm', this.key, iv);
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
const tag = cipher.getAuthTag();
return {
ciphertext: encrypted.toString('base64'),
iv: iv.toString('base64'),
tag: tag.toString('base64'),
};
}
decrypt(ciphertextB64: string, ivB64: string, tagB64: string): string {
const ciphertext = Buffer.from(ciphertextB64, 'base64');
const iv = Buffer.from(ivB64, 'base64');
const tag = Buffer.from(tagB64, 'base64');
const decipher = createDecipheriv('aes-256-gcm', this.key, iv);
decipher.setAuthTag(tag);
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8');
}
}
2. Store encrypted objects as a single DynamoDB attribute
Avoid splitting ciphertext, IV, and tag across multiple attributes, which can lead to inconsistent handling. Store the encrypted payload as a single Base64-encoded structure.
import { DynamoDB } from '@aws-sdk/client-dynamodb';
import { marshall, unmarshall } from '@aws-sdk/util-dynamodb';
import { SecureDynamoDBService } from './SecureDynamoDBService';
const ddb = new DynamoDB({});
const cryptoService = new SecureDynamoDBService();
export const storeSession = async (userId: string, data: object) => {
const plaintext = JSON.stringify(data);
const encrypted = cryptoService.encrypt(plaintext);
const params = {
TableName: process.env.SESSION_TABLE!,
Item: marshall({
pk: `USER#${userId}`,
sk: 'session#current',
...encrypted,
}),
};
await ddb.put(params).then(() => ({ ok: true }));
};
export const getSession = async (userId: string) => {
const params = {
TableName: process.env.SESSION_TABLE!,
Key: marshall({ pk: `USER#${userId}`, sk: 'session#current' }),
};
const { Item } = await ddb.get(params);
if (!Item) throw new Error('Not found');
const unmarshalled = unmarshall(Item);
try {
const payload = cryptoService.decrypt(unmarshalled.ciphertext, unmarshalled.iv, unmarshalled.tag);
return JSON.parse(payload);
} catch (err) {
// Always return a generic error to avoid oracle
throw new Error('Failed to decrypt');
}
};
3. Centralize error handling in Adonisjs middleware
Ensure that all exceptions from DynamoDB or decryption are mapped to a generic response. This prevents attackers from distinguishing padding errors from other failures.
// start/hooks/index.ts
import { ExceptionHandler } from '@poppinss/dev-utils';
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
export const exceptionHandler = new ExceptionHandler();
exceptionHandler.register('CryptoException', (_ctx, _error) => {
_ctx.response.status(400).send({ error: 'Invalid request' });
});
exceptionHandler.register('DatabaseException', (_ctx, _error) => {
_ctx.response.status(500).send({ error: 'Internal server error' });
});
// In a route handler
try {
const session = await getSession(userId);
} catch (error) {
// The exception handler will normalize the response
throw error;
}
4. DynamoDB conditional writes and integrity checks
When storing encrypted items, use a checksum or authentication tag stored as an attribute to detect tampering before decryption. This reduces the risk of manipulated ciphertext being submitted to the decryption oracle.
import { marshall } from '@aws-sdk/util-dynamodb';
const item = {
pk: 'USER#123',
sk: 'session#abc',
ciphertext: '...',
iv: '...',
tag: '...',
checksum: 'sha256:abcdef...', // computed over ciphertext+iv
};
const params = {
TableName: 'Sessions',
Item: marshall(item),
ConditionExpression: 'attribute_not_exists(pk)',
};
await ddb.put(params).catch((err) => {
if (err.name === 'ConditionalCheckFailedException') {
// handle conflict/tampering suspicion
}
});