Padding Oracle in Express with Basic Auth
Padding Oracle in Express with Basic Auth — how this specific combination creates or exposes the vulnerability
Padding oracle vulnerabilities arise when an application reveals whether decryption padding is valid during error handling or response differences. In Express, using HTTP Basic Auth does not encrypt the credentials in transit; if the application then stores or reuses those credentials as a key, or derives a key from them, the interaction can expose a padding oracle when ciphertext is supplied by the client and the server distinguishes between padding errors and other failures.
Consider an Express route that receives an encrypted payload (for example, a token or stored preference) and a password provided via Basic Auth. If the server decrypts the payload using a key derived from the Basic Auth password and returns different HTTP status codes or response bodies for padding errors versus other errors, an attacker can iteratively decrypt or forge messages by observing these distinctions. This is especially risky when the same secret or derived key is used across requests, and when error handling is verbose, leaking the nature of the failure to the client.
In the context of the 12 checks run by middleBrick, this scenario would surface in the Authentication and Input Validation findings. The scanner tests whether error handling is consistent across malformed or manipulated ciphertexts, and whether authentication mechanisms leak information that could assist an attacker. An unauthenticated scan can detect endpoints where the response differs in timing or content based on padding validity, and map the issue to OWASP API Top 10 and relevant compliance frameworks.
Basic Auth-Specific Remediation in Express — concrete code fixes
Remediation focuses on avoiding the use of user-supplied credentials as cryptographic keys, ensuring constant-time comparison where padding is validated, and standardizing error responses. Do not derive encryption keys directly from Basic Auth passwords; instead, use a strong, randomly generated key stored securely and reference it independently of authentication.
Below are concrete Express examples. The insecure example shows a common pitfall; the secure example demonstrates remediation.
Insecure Express route using Basic Auth to derive a key
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
app.post('/insecure-decrypt', (req, res) => {
const auth = req.headers.authorization || '';
const match = auth.match(/^Basic\s+(\S+)$/);
if (!match) {
return res.status(401).json({ error: 'Unauthorized' });
}
const buffer = Buffer.from(match[1], 'base64');
const [user, pass] = buffer.toString().split(':');
// Insecure: using password directly as key material
const key = crypto.createHash('sha256').update(pass).digest();
const iv = Buffer.alloc(16, 0);
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
let decrypted;
try {
decrypted = Buffer.concat([decipher.update(req.body.ciphertext, 'base64'), decipher.final()]);
res.json({ data: decrypted.toString() });
} catch (err) {
// Insecure: different error paths can leak padding oracle info
res.status(400).json({ error: err.message });
}
});
This route uses the password-derived key directly and returns the error message from the decipher, which can expose padding-related failures and enable an oracle attack.
Secure Express route with constant-time verification and independent key
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
// Use a strong, randomly generated key stored in environment variables or a secrets manager
const INDEPENDENT_KEY = Buffer.from(process.env.ENCRYPTION_KEY_HEX, 'hex'); // 32 bytes for aes-256
function timingSafeEqual(a, b) {
if (a.length !== b.length) {
return false;
}
return crypto.timingSafeEqual(a, b);
}
app.post('/secure-decrypt', (req, res) => {
const auth = req.headers.authorization || '';
const match = auth.match(/^Basic\s+(\S+)$/);
if (!match) {
return res.status(401).json({ error: 'Unauthorized' });
}
const buffer = Buffer.from(match[1], 'base64');
const [user, pass] = buffer.toString().split(':');
// Authenticate the user via a secure store; do not use pass as a key
const validUser = authenticateUser(user, pass); // implement your own secure check
if (!validUser) {
return res.status(401).json({ error: 'Unauthorized' });
}
const iv = Buffer.alloc(16, 0);
const ciphertext = req.body.ciphertext;
if (!ciphertext || typeof ciphertext !== 'string') {
return res.status(400).json({ error: 'Bad request' });
}
const buf = Buffer.from(ciphertext, 'base64');
const decipher = crypto.createDecipheriv('aes-256-cbc', INDEPENDENT_KEY, iv);
let decrypted;
try {
decrypted = Buffer.concat([decipher.update(buf), decipher.final()]);
} catch (err) {
// Use a generic, consistent error to avoid leaking padding or decryption details
return res.status(400).json({ error: 'Bad request' });
}
// If you need to verify integrity, use an HMAC and compare in constant time
res.json({ data: decrypted.toString() });
});
function authenticateUser(user, pass) {
// Replace with a secure user/password check (e.g., hashed password in DB)
// This is a placeholder to illustrate separation of concerns
return user === 'alice' && timingSafeEqual(crypto.createHash('sha256').update(pass).digest(), crypto.createHash('sha256').update('correcthorsebatterystaple').digest());
}
Key points in the secure version: the encryption key is independent of the Basic Auth credentials; errors are generic and consistent; and cryptographic operations avoid branching on secret-dependent data where possible. This reduces the risk of a padding oracle while keeping Basic Auth for authentication.