Timing Attack in Adonisjs with Mutual Tls
Timing Attack in Adonisjs with Mutual Tls — how this specific combination creates or exposes the vulnerability
A timing attack in AdonisJS when mutual TLS (mTLS) is in use arises from observable differences in request processing time based on the correctness of the client certificate validation and subsequent authentication steps. Even with mTLS enforcing certificate-based client authentication, the server-side logic that follows certificate verification can introduce variable execution paths. For example, after the TLS handshake completes successfully, AdonisJS may perform additional checks such as verifying the certificate against an allowlist, mapping the certificate to a user, or enforcing group or role constraints. If these checks are implemented using non-constant-time comparisons—such as iterating over an array of allowed certificates with a standard loop or comparing certificate fingerprints using a naive string equality—attackers can measure response times to infer which certificates or users are valid.
Mutual TLS shifts some trust to the client certificate, but it does not eliminate injection or authorization risks. An endpoint that accepts mTLS may still be vulnerable to timing attacks if it processes authenticated identities differently based on data retrieved from the certificate (e.g., Common Name or Subject Alternative Name). Suppose the server performs a database lookup using a value extracted from the certificate. In that case, the time taken to execute SQL queries can vary depending on whether the user exists, the number of rows scanned, or whether additional filtering is applied. These variations can be detected by an attacker controlling the client certificate and measuring round-trip times across many requests.
The combination of mTLS and application-level authorization in AdonisJS can inadvertently create a side channel. Consider an endpoint that first validates the client certificate and then conditionally loads user-specific data. If the logic branches—such as returning early for unauthorized certificate mappings versus performing a full data retrieval—these branches may execute in different amounts of time. An attacker with the ability to present different certificates can correlate timing differences with the presence or absence of data, effectively probing the authorization boundary. This becomes particularly relevant when using features like ACL scopes or row-level security that are applied after authentication and may exhibit timing variability based on policy evaluation.
Input validation and serialization also contribute to timing differences. AdonisJS often relies on schema validation (for example, using Joi or Yup-like rules) and serialization routines to shape responses. If validation or serialization paths differ based on certificate-derived claims—such as validating different payloads for different user groups—the processing time can vary. Although these differences are typically small, an attacker conducting a precise measurement across many requests can infer details about the server’s internal logic, such as which fields are required for certain certificate types or which transformations are applied.
To assess such risks, tools like middleBrick can analyze the unauthenticated attack surface of an AdonisJS API, including endpoints protected by mTLS. While mTLS provides strong transport-layer identity, it does not inherently prevent timing attacks at the application layer. middleBrick runs checks for authentication mechanisms, input validation, and rate limiting, and it can surface findings related to inconsistent processing times that may expose sensitive logic. By correlating TLS-level identity handling with observable behavior, security assessments can highlight where constant-time practices and strict authorization boundaries are necessary.
Mutual Tls-Specific Remediation in Adonisjs — concrete code fixes
Remediation focuses on ensuring that all steps following certificate validation execute in constant time and that authorization decisions do not leak information via timing differences. Below are concrete code examples for AdonisJS that demonstrate mTLS configuration and secure handling of certificate-derived identity.
1. Configure mTLS in AdonisJS (server-side)
AdonisJS uses an underlying HTTPS server. You can enforce client certificate verification through the server configuration. The following example shows setting up an HTTPS server with requestCert and rejectUnauthorized enabled, ensuring that only clients with valid certificates are accepted.
const https = require('https');
const fs = require('fs');
const { Ignitor } = require('@adonisjs/ignitor');
const server = https.createServer({
key: fs.readFileSync('path/to/server.key'),
cert: fs.readFileSync('path/to/server.crt'),
ca: fs.readFileSync('path/to/ca.crt'),
requestCert: true,
rejectUnauthorized: true,
}, (req, res) => {
// AdonisJS bootstrap
new Ignitor(require('@adonisjs/fold')).httpServer().start(req, res);
});
server.listen(443);
This configuration forces the TLS layer to require and validate client certificates. Note that validation happens at the transport layer; application logic must still treat the certificate as untrusted input and validate it further.
2. Constant-time certificate fingerprint comparison
After the TLS handshake, you may map the client certificate to an internal user or role. Avoid branching on equality checks that can leak information. Use a constant-time comparison for fingerprints or identifiers extracted from the certificate.
const crypto = require('crypto');
function constantTimeEquals(a, b) {
return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
}
// Example: extract SHA-256 fingerprint from client certificate
function getCertFingerprint(certPem) {
const hash = crypto.createHash('sha256').update(certPem).digest('hex');
return hash;
}
// In your request handler
const clientCert = req.socket.getPeerCertificate();
const fingerprint = getCertFingerprint(clientCert.raw.toString('base64'));
const allowedFingerprints = [
'a1b2c3d4e5f6...',
'0a9b8c7d6e5f...',
];
let authorized = false;
for (const allowed of allowedFingerprints) {
if (constantTimeEquals(fingerprint, allowed)) {
authorized = true;
break;
}
}
if (!authorized) {
res.statusCode = 403;
res.end('Forbidden');
return;
}
This approach ensures that the comparison time does not depend on how many allowed fingerprints exist, mitigating timing-based inference attacks.
3. Avoid branching on certificate-derived data in authorization
When mapping certificates to users, perform lookups that do not expose timing differences. Use parameterized queries or ORM methods that execute the same path regardless of whether a match exists.
const User = use('App/Models/User');
async function getUserByCertFingerprint(fingerprint) {
// Use a query that does not branch on existence
const user = await User.query()
.where('cert_fingerprint', fingerprint)
.first();
return user;
}
// In request handler
const user = await getUserByCertFingerprint(fingerprint);
if (!user) {
// Still return a generic response with similar timing characteristics
res.statusCode = 403;
res.end('Forbidden');
return;
}
// Proceed with user-specific logic
req.user = user;
Ensure that error paths and success paths take comparable time. For example, avoid early returns that skip middleware or serialization steps that would otherwise execute. Instead, structure the handler so that the same stages run regardless of identity checks.
4. Consistent validation and serialization
Apply the same validation and serialization routines regardless of certificate claims. If different policies apply to different users, ensure that the computational cost of evaluating those policies is bounded and does not vary significantly.
const { validate } = use('Validator');
const schema = {
action: 'string',
resource_id: 'required|integer',
};
async function handleRequest(req, res) {
const validation = await validate(req.all(), schema);
if (validation.fails()) {
res.statusCode = 400;
res.json({ errors: validation.messages() });
return;
}
// Continue processing with consistent overhead
const payload = req.only(['action', 'resource_id']);
// ...
}
By combining mTLS with constant-time practices, predictable authorization flows, and uniform validation, you reduce the risk of timing attacks while retaining the security benefits of mutual authentication.