Padding Oracle in Express with Cockroachdb
Padding Oracle in Express with Cockroachdb — how this specific combination creates or exposes the vulnerability
A padding oracle in an Express API that uses Cockroachdb typically arises when error handling for encrypted data reveals whether a ciphertext is valid before decryption completes. If your endpoints accept encrypted payloads (for example, a token or a JSON field encrypted with AES), and the server returns distinct errors for bad padding versus other failures, an attacker can iteratively submit manipulated ciphertexts and infer validity from timing or status differences. When the same endpoint also issues SQL against Cockroachdb, the interaction can amplify information leakage: a malformed ciphertext may cause decryption to fail early, but a ciphertext with valid padding but invalid structure may trigger a database query that fails differently depending on SQL errors, constraints, or whether a row is found.
Consider an Express route that decrypts a cookie or header, then queries Cockroachdb using the decrypted user identifier:
const crypto = require('crypto');
const express = require('express');
const { Pool } = require('pg'); // node-postgres driver for Cockroachdb
const app = express();
app.use(express.json());
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
function decryptPayload(enc, key) {
const iv = enc.slice(0, 16);
const ciphertext = enc.slice(16);
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
// This can throw if padding is invalid
return Buffer.concat([decipher.update(ciphertext), decipher.final());
}
app.get('/user/profile', async (req, res) => {
const token = req.headers['x-auth-token'];
if (!token) return res.status(400).send('Missing token');
let key = deriveKey(); // omitted derivation details
let userId;
try {
const plain = decryptPayload(Buffer.from(token, 'base64'), key);
userId = plain.toString();
} catch (e) {
return res.status(400).send('Invalid token');
}
try {
const { rows } = await pool.query('SELECT email, role FROM users WHERE id = $1', [userId]);
if (rows.length === 0) return res.status(404).send('User not found');
res.json(rows[0]);
} catch (dbErr) {
// Distinct SQL error can hint at valid padding but invalid data
return res.status(500).send('Database error');
}
});
In this setup, a padding oracle can emerge because:
- Decryption errors are surfaced differently from SQL errors. For instance, an invalid padding throws during
decipher.final(), while a valid padding but non-existent user yields a SQL error from Cockroachdb with a different message or status code. - If the route leaks timing differences (e.g., decryption and padding validation complete faster than when the database is queried), an attacker can correlate response times with ciphertext validity.
- An attacker who can observe whether a request fails at the decryption stage or the database stage can gradually decrypt or forge tokens without needing the key, especially if error messages or HTTP status codes differ between padding failures and SQL constraint violations.
Even when using Cockroachdb’s secure TLS connections and parameterized queries to avoid SQL injection, the distinction between application-layer decryption errors and database-layer errors remains a risk. The presence of Cockroachdb does not cause the padding oracle, but the way errors are handled across these layers can make the oracle practical: an attacker can send crafted ciphertexts, observe whether the failure occurs before or after the database call, and refine their guesses byte by byte.
Cockroachdb-Specific Remediation in Express — concrete code fixes
Remediation focuses on making error handling consistent and avoiding any branching based on whether decryption or database operations fail. Treat all failures as generic server errors and ensure timing does not depend on secret-dependent branches.
1) Use constant-time comparison and unified error paths
Do not expose whether padding failed versus SQL failure. After decryption, validate structure without branching on content, and use a single endpoint response for all client errors where appropriate.
app.get('/user/profile-safe', async (req, res) => { const token = req.headers['x-auth-token']; if (!token) return res.status(400).send('Bad request'); let key = deriveKey(); let userId; try { const plain = decryptPayload(Buffer.from(token, 'base64'), key); userId = plain.toString(); } catch (e) { // Generic failure; do not distinguish padding vs other errors return res.status(400).send('Invalid token'); } try { const { rows } = await pool.query('SELECT email, role FROM users WHERE id = $1', [userId]); if (rows.length === 0) { // Return same shape and status as above to avoid timing/behavioral leaks return res.status(404).json({ email: '', role: '' }); } res.json(rows[0]); } catch (dbErr) { // Log for investigation, but return generic error to the client console.error(dbErr); return res.status(500).send('Internal server error'); } });2) Use authenticated encryption with associated data (AEAD)
Prefer AES-GCM which provides integrity and authentication, reducing reliance on padding and making padding oracle concerns less relevant. If you must use CBC, ensure you use a verified library that handles padding verification consistently and does not throw distinct errors for padding versus MAC failures.
function decryptPayloadAead(enc, key) { const nonce = enc.slice(0, 12); const ciphertext = enc.slice(12); const decipher = crypto.createDecipheriv('aes-256-gcm', key, nonce); // No padding issues with GCM; authentication tag is typically appended return Buffer.concat([decipher.update(ciphertext), decipher.final()]); }3) Avoid branching on user-controlled data before DB queries
Ensure that operations that may fail (like DB queries) do not create observable timing differences based on secret-derived values. If you must check existence, use a constant-time dummy query path when the user is not found.
app.get('/profile-timing-safe', async (req, res) => { const token = req.headers['x-auth-token']; if (!token) return res.status(400).send('Bad request'); let key = deriveKey(); let userId; try { const plain = decryptPayload(Buffer.from(token, 'base64'), key); userId = plain.toString(); } catch (e) { return res.status(400).send('Invalid token'); } // Always query with the provided userId; do not skip query when missing try { const { rows } = await pool.query('SELECT email, role FROM users WHERE id = $1', [userId]); // Even if rows is empty, return a generic response shape const row = rows.length ? rows[0] : { email: '', role: '' }; res.json(row); } catch (dbErr) { console.error(dbErr); res.status(500).send('Internal server error'); } });4) Parameterized queries and connection hygiene
Always use parameterized queries with Cockroachdb to avoid SQL injection, and ensure connections are properly managed. This does not directly stop a padding oracle, but it keeps the database layer predictable and avoids secondary information leaks through SQL error messages.
// Example of safe parameterized usage await pool.query('INSERT INTO audit_log(user_id, action) VALUES ($1, $2)', [userId, 'profile_view']);By standardizing error responses, avoiding early exits based on padding failures, and using modern authenticated encryption where feasible, you reduce the practical impact of a padding oracle in an Express service backed by Cockroachdb.