Bleichenbacher Attack in Strapi with Basic Auth
Bleichenbacher Attack in Strapi with Basic Auth — how this specific combination creates or exposes the vulnerability
A Bleichenbacher attack is a chosen-ciphertext attack against asymmetric encryption (e.g., RSA) that recovers plaintext by observing server behavior to many adaptive decryption attempts. When Basic Auth is used in Strapi, credentials are often transmitted as a base64-encoded header Authorization: Basic base64(username:password). If Strapi internally uses RSA-encrypted tokens or API keys to validate Basic Auth credentials — for example, validating a JWT signed with RSA or using RSA-wrapped secrets — and returns distinct timing or error responses for padding vs. other decryption errors, the combination creates a Bleichenbacher vector.
Strapi’s admin API endpoints that rely on encrypted credentials or signed tokens can become vulnerable when Basic Auth is accepted alongside RSA-based validation. An attacker can enumerate valid users by observing timing differences or error message variations between valid and invalid ciphertexts. This risk is higher when Strapi is exposed unauthenticated (black-box scanning) and the server’s error handling leaks whether a decryption or padding check failed. Strapi’s default behavior may include informative validation errors, which, when combined with predictable RSA padding checks, allows adaptive queries to gradually recover the plaintext key material or session token without ever needing direct access to the private key.
During a black-box scan, middleBrick tests for information leakage and inconsistent response behavior across authentication schemes. One of the 12 parallel checks analyzes whether error messages, response times, or status codes differ based on authentication inputs, which could indicate a Bleichenbacher-style oracle. For instance, if Strapi returns 401 for a malformed token and 403 for a padding error, an attacker can distinguish outcomes and iteratively decrypt or forge credentials. The scanner also checks whether RSA-based operations are performed on user-supplied data in unauthenticated contexts, which is a common precursor to this class of issue.
In practice, this means an attacker who can send many crafted Basic Auth headers — or manipulate encrypted parameters that Strapi validates via RSA — can gradually decrypt or forge authentication material. This violates the principle that authentication mechanisms should fail consistently and without leaking cryptographic side channels. The presence of Basic Auth alone does not cause Bleichenbacher; it is the interaction with RSA-based validation and noisy server responses that enables the attack.
Basic Auth-Specific Remediation in Strapi — concrete code fixes
Remediation focuses on removing RSA-based asymmetric operations for credential validation, ensuring constant-time comparisons, and avoiding information leakage in authentication flows. Do not rely on RSA decryption to verify Basic Auth credentials; instead use symmetric, constant-time checks or built-in identity providers. Below are concrete Strapi code examples that follow these principles.
Example 1: Use symmetric comparison for API keys instead of RSA
// config/middleware.js
module.exports = {
settings: {
// Disable any RSA-based auth helpers that perform decryption
// Use a constant-time comparison for symmetric API keys
apiKeyCheck: async (providedKey) => {
const expectedKey = strapi.config.get('api.symmetricKey');
// crypto.timingSafeEqual prevents timing attacks; both keys must be buffers of same length
const providedBuf = Buffer.from(providedKey, 'hex');
const expectedBuf = Buffer.from(expectedKey, 'hex');
if (providedBuf.length !== expectedBuf.length) {
return false;
}
return crypto.timingSafeEqual(providedBuf, expectedBuf);
},
},
};
Example 2: Validate Basic Auth with a constant-time password check
// plugins/users-permissions/config/policies.js
module.exports = {
policies: {
'auth/local': ['sanitize', 'validate-basic-auth'],
},
};
// plugins/users-permissions/policies/validate-basic-auth.js
const crypto = require('crypto');
module.exports = {
handler: async (ctx, next) => {
const authHeader = ctx.request.header.authorization || '';
const base64 = authHeader.replace(/^Basic\s+/i, '');
const decoded = Buffer.from(base64, 'base64').toString('utf8');
const [username, password] = decoded.split(':');
if (!username || !password) {
ctx.status = 401;
return { error: 'Invalid authorization header' };
}
const user = await strapi.entityService.findOne('api::user.user', { username });
if (!user) {
// Use constant-time hash compare to avoid timing leaks
const dummyHash = crypto.randomBytes(64);
await new Promise((r) => setTimeout(r, 10)); // simulate work
ctx.status = 401;
return { error: 'Invalid credentials' };
}
// Assume user.password is a bcrypt hash
const match = await strapi.plugin('users-permissions').service('users').comparePassword(password, user.password);
if (!match) {
ctx.status = 401;
return { error: 'Invalid credentials' };
}
await next();
},
};
Example 3: Disable verbose authentication errors in Strapi responses
// config/middleware.js
module.exports = {
settings: {
// Ensure authentication failures return generic responses
authenticate: (config, { strapi }) => {
return async (ctx, next) => {
try {
await next();
} catch (err) {
if (err.name === 'UnauthorizedError') {
ctx.status = 401;
ctx.body = { error: 'Unauthorized' }; // Generic message, no stack or detail
} else {
throw err;
}
}
};
},
},
};
Additionally, prefer modern authentication mechanisms over Basic Auth where possible — for example, use JWT with a strong symmetric algorithm (HS256) or OAuth2 flows. If RSA-based signing is required (e.g., JWT with RS256), ensure private keys never participate in online validation and that libraries use constant-time padding verification. middleBrick’s scans include checks for authentication inconsistency and information leakage, so applying these fixes reduces the attack surface and aligns findings with remediation guidance provided in the dashboard, CLI, and GitHub Action integrations.